From 92b4bb612b90fa8fb4a31073074dae3b5b975b69 Mon Sep 17 00:00:00 2001 From: onsetsu Date: Tue, 9 Apr 2024 17:13:56 +0200 Subject: [PATCH] dragging cards SQUASHED: AUTO-COMMIT-demos-stefan-untitled-board-game-ubg-card.js,AUTO-COMMIT-demos-stefan-untitled-board-game-ubg-cards-exporter.js,AUTO-COMMIT-src-client-clipboard.js,AUTO-COMMIT-src-components-tools-lively-container.js,AUTO-COMMIT-src-components-tools-lively-container-navbar.html,AUTO-COMMIT-src-components-tools-lively-container-navbar.js,AUTO-COMMIT-src-components-widgets-ubg-card.html,AUTO-COMMIT-src-components-widgets-ubg-card.js,AUTO-COMMIT-src-components-widgets-ubg-cards-editor.html,AUTO-COMMIT-src-components-widgets-ubg-cards-editor.js,AUTO-COMMIT-src-components-widgets-ubg-cards-entry.html,AUTO-COMMIT-src-components-widgets-ubg-cards-entry.js,AUTO-COMMIT-src-components-widgets-ubg-cards.html,AUTO-COMMIT-src-components-widgets-ubg-cards.js, --- demos/stefan/untitled-board-game/ubg-card.js | 15 + .../untitled-board-game/ubg-cards-exporter.js | 9 +- src/client/clipboard.js | 2 + .../tools/lively-container-navbar.js | 1 + src/components/tools/lively-container.js | 2 +- src/components/widgets/ubg-card.html | 2 +- src/components/widgets/ubg-card.js | 1699 ++----------- src/components/widgets/ubg-cards-editor.html | 68 +- src/components/widgets/ubg-cards-editor.js | 77 +- src/components/widgets/ubg-cards-entry.html | 2 + src/components/widgets/ubg-cards-entry.js | 29 + src/components/widgets/ubg-cards.html | 46 +- src/components/widgets/ubg-cards.js | 2120 +---------------- 13 files changed, 510 insertions(+), 3562 deletions(-) diff --git a/demos/stefan/untitled-board-game/ubg-card.js b/demos/stefan/untitled-board-game/ubg-card.js index ba84ca66e..ca1775a74 100644 --- a/demos/stefan/untitled-board-game/ubg-card.js +++ b/demos/stefan/untitled-board-game/ubg-card.js @@ -27,6 +27,18 @@ export default class Card { this.versions.last.name = name; } + getIdentity() { + return this.identity; + } + + setIdentity(identity) { + if (identity === undefined) { + delete this.identity; + } else { + this.identity = identity; + } + } + getType() { return this.versions.last.type; } @@ -183,4 +195,7 @@ export default class Card { return this.versions.length; } + toString() { + return `Card ${this.getName() || this.getId()}` + } } \ No newline at end of file diff --git a/demos/stefan/untitled-board-game/ubg-cards-exporter.js b/demos/stefan/untitled-board-game/ubg-cards-exporter.js index 947521301..c1f2e6edd 100644 --- a/demos/stefan/untitled-board-game/ubg-cards-exporter.js +++ b/demos/stefan/untitled-board-game/ubg-cards-exporter.js @@ -35,15 +35,13 @@ export default class CardExporter { } /*MD ## Layout & Rendering MD*/ - static async execute(cards, ubgCards) { - const cardsToPrint = cards.slice(0, 14) - + static async execute(cardsToPrint, ubgCards, skipCardBack) { this.printWithSavedWorld(async () => { const body = document.body body.innerHTML = '' // body.style = "" - await this.buildCards(undefined, cardsToPrint, false, ubgCards) + await this.buildCards(undefined, cardsToPrint, skipCardBack, ubgCards) }) } @@ -70,7 +68,8 @@ box-shadow: inset 0px 0px 0px 2px black; static createCardPreview(card, ubgCards) { const cardPreview = document.createElement('ubg-card') cardPreview.setCard(card) - cardPreview.src = ubgCards.src + cardPreview.setCards(ubgCards.cards) + cardPreview.setSrc(ubgCards.src) return cardPreview } diff --git a/src/client/clipboard.js b/src/client/clipboard.js index 90b419012..a8360b762 100644 --- a/src/client/clipboard.js +++ b/src/client/clipboard.js @@ -2,6 +2,7 @@ /* global that */ + import {pt} from 'src/client/graphics.js'; import Halo from "src/components/halo/lively-halo.js"; import { uuid } from 'utils'; @@ -9,6 +10,7 @@ import persistence from "src/client/persistence.js" import {default as HaloService} from "src/components/halo/lively-halo.js" + export default class Clipboard { static load() { diff --git a/src/components/tools/lively-container-navbar.js b/src/components/tools/lively-container-navbar.js index 6311723b1..7bdf33f96 100644 --- a/src/components/tools/lively-container-navbar.js +++ b/src/components/tools/lively-container-navbar.js @@ -1022,6 +1022,7 @@ export default class LivelyContainerNavbar extends Morph { eaMethodInfo.class = classInfo.name // for later use var name = eaMethodInfo.name var methodItem = this.createDetailsItem(name) + debugger if (eaMethodInfo.static) { methodItem.insertBefore(static, methodItem.querySelector("a")) } diff --git a/src/components/tools/lively-container.js b/src/components/tools/lively-container.js index 6c5c845ee..cbd679bb8 100644 --- a/src/components/tools/lively-container.js +++ b/src/components/tools/lively-container.js @@ -474,7 +474,7 @@ export default class Container extends Morph { if (render) { return this.appendHtml('', renderTimeStamp); } - } else if (format == "json" && files.name(url).startsWith('all-cards')) { + } else if (format == "json" && files.name(url).includes('all-cards')) { this.sourceContent = content; if (render) { return this.appendHtml('', renderTimeStamp); diff --git a/src/components/widgets/ubg-card.html b/src/components/widgets/ubg-card.html index c66273d06..7fb4f6bfe 100644 --- a/src/components/widgets/ubg-card.html +++ b/src/components/widgets/ubg-card.html @@ -22,7 +22,7 @@ background-position: center center; } #outer { - box-shadow: inset 0px 0px 0px 2px #0f0; +/* box-shadow: inset 0px 0px 0px 2px #0f0; */ width: 100%; height: 100%; /* background-color: transparent; diff --git a/src/components/widgets/ubg-card.js b/src/components/widgets/ubg-card.js index 6a4fc5f26..a6773d22d 100644 --- a/src/components/widgets/ubg-card.js +++ b/src/components/widgets/ubg-card.js @@ -2,140 +2,14 @@ import Morph from 'src/components/widgets/lively-morph.js'; -import ContextMenu from 'src/client/contextmenu.js'; -import "src/external/pdf.js"; -import { shake } from 'utils'; import { Point } from 'src/client/graphics.js' import paper from 'src/client/paperjs-wrapper.js' -import 'https://lively-kernel.org/lively4/ubg-assets/load-assets.js'; - -import { serialize, deserialize } from 'src/client/serialize.js'; -import Card from 'demos/stefan/untitled-board-game/ubg-card.js'; +// import 'https://lively-kernel.org/lively4/ubg-assets/load-assets.js'; const POKER_CARD_SIZE_INCHES = lively.pt(2.5, 3.5); const POKER_CARD_SIZE_MM = POKER_CARD_SIZE_INCHES.scaleBy(25.4); -class FontCache { - - constructor() { - this.fonts = {}; - } - - /*MD ## Font Loading & Parsing MD*/ - async getFile(path) { - if (this.fonts[path]) { - lively.notify('cache hit') - } else { - lively.notify('cache miss') - this.fonts[path] = this.getBase64Font(path) - } - - return this.fonts[path]; - } - - async getBase64Font(url) { - async function blobToBase64String(blob) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - // Extract the Base64 encoded string - const base64String = reader.result.split(',')[1]; - resolve(base64String); - }; - reader.readAsDataURL(blob); - }) - } - - const response = await fetch(url) - if (!response.ok) { - throw new Error('Network response was not ok.'); - } - const blob = await response.blob(); - return blobToBase64String(blob) - } - - /*MD ## Font Asset Locations MD*/ - get FONT_ASSETS_FOLDER() { - return 'https://lively-kernel.org/lively4/ubg-assets/fonts/'; - } - - async getFontAwesomeFont(fileName) { - const FONT_AWESOME_FONT_FOLDER = this.FONT_ASSETS_FOLDER + 'fontawesome-v6.1.1/webfonts/'; - return this.getBase64Font(FONT_AWESOME_FONT_FOLDER + fileName) - } - - async BASE64_FONT_AWESOME_THIN() { - return this.getFontAwesomeFont('fa-thin-100.ttf') - } - async BASE64_FONT_AWESOME_LIGHT() { - return this.getFontAwesomeFont('fa-light-300.ttf') - } - async BASE64_FONT_AWESOME_REGULAR() { - return this.getFontAwesomeFont('fa-regular-400.ttf') - } - async BASE64_FONT_AWESOME_SOLID() { - return this.getFontAwesomeFont('fa-solid-900.ttf') - } - async BASE64_FONT_AWESOME_BRANDS() { - return this.getFontAwesomeFont('fa-brands-400.ttf') - } - async BASE64_FONT_AWESOME_DUOTONE() { - return this.getFontAwesomeFont('fa-duotone-900.ttf') - } - - async getRuneterraFont(fileName) { - const RUNETERRA_FONT_FOLDER = this.FONT_ASSETS_FOLDER + 'runeterra/fonts/'; - return this.getBase64Font(RUNETERRA_FONT_FOLDER + fileName) - } - - async BASE64_BeaufortforLOLJaBold() { - return this.getRuneterraFont('BeaufortforLOLJa-Bold.ttf') - } - async BASE64_BeaufortforLOLJaRegular() { - return this.getRuneterraFont('BeaufortforLOLJa-Regular.ttf') - } - async BASE64_Univers_55() { - return this.getRuneterraFont('univers_55.ttf') - } - async BASE64_Univers45LightItalic() { - return this.getRuneterraFont('univers-45-light-italic.ttf') - } - -} - -if (globalThis.__ubg_font_cache__) { - globalThis.__ubg_font_cache__.migrateTo(FontCache); -} else { - globalThis.__ubg_font_cache__ = new FontCache(); -} - -const FONT_NAME_FA_THIN_100 = 'fa-thin-100' -const FONT_NAME_FA_LIGHT_300 = 'fa-light-300' -const FONT_NAME_FA_REGULAR_400 = 'fa-regular-400' -const FONT_NAME_FA_SOLID_900 = 'fa-solid-900' -const FONT_NAME_FA_BRANDS_400 = 'fa-brands-400' -const FONT_NAME_FA_DUOTONE_900 = 'fa-duotone-900' - -const FONT_NAME_BEAUFORT_FOR_LOL_BOLD = 'BeaufortforLOLJa-Bold' -const FONT_NAME_BEAUFORT_FOR_LOL_REGULAR = 'BeaufortforLOLJa-Regular' -const FONT_NAME_UNIVERS_55 = 'univers_55' -const FONT_NAME_UNIVERS_45_LIGHT_ITALIC = 'Univers 45 Light Italic' - -// Card group name (ELITE, SPIDER, YETI, etc.) -- Univers 59 // #BROKEN?? #TODO -const FONT_NAME_CARD_TYPE = FONT_NAME_UNIVERS_55 - -// Card description -- Univers 55 -const FONT_NAME_CARD_TEXT = FONT_NAME_UNIVERS_55 - -const RUNETERRA_FONT_ID = 'runeterra-fonts' -// lively.loadCSSThroughDOM(RUNETERRA_FONT_ID, 'https://lively-kernel.org/lively4/ubg-assets/fonts/runeterra/css/runeterra.css') - -const CSS_CLASS_BEAUFORT_FOR_LOL_BOLD = 'beaufort-for-lol-bold' -const CSS_CLASS_BEAUFORT_FOR_LOL_REGULAR = 'beaufort-for-lol-regular' -const CSS_CLASS_UNIVERS_55 = 'univers-55' -const CSS_CLASS_UNIVERS_45_LIGHT_ITALIC = 'univers-45-light-italic' - const CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD = "Beaufort for LOL Bold" const CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_REGULAR = "Beaufort for LOL Regular" const CSS_FONT_FAMILY_UNIVERS_55 = "Univers 55" @@ -145,7 +19,11 @@ const CSS_FONT_FAMILY_UNIVERS_45_LIGHT_ITALIC = "Univers 45 Light Italic" const CSS_FONT_FAMILY_CARD_NAME = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD const CSS_FONT_FAMILY_CARD_COST = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD const CSS_FONT_FAMILY_CARD_VP = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_BOLD -const CSS_FONT_FAMILY_CARD_TYPE = CSS_FONT_FAMILY_UNIVERS_55 +const CSS_FONT_FAMILY_CARD_TYPE = CSS_FONT_FAMILY_BEAUFORT_FOR_LOL_REGULAR +// #TODO: Card group name (ELITE, SPIDER, YETI, etc.) -- Univers 59 // #BROKEN?? #TODO + +// Card description -- Univers 55 +const CSS_FONT_FAMILY_CARD_TEXT = CSS_FONT_FAMILY_UNIVERS_55 function identity(value) { return value; @@ -166,57 +44,6 @@ function mmToPoint() { return this * 2.835; } -function isAsync(fn) { - return fn.constructor === (async () => {}).constructor; -} - -/* `this` is a jspdf doc */ -function withGraphicsState(cb) { - if (isAsync(cb)) { - return (async () => { - this.saveGraphicsState(); // this.internal.write('q'); - try { - return await cb(); - } finally { - this.restoreGraphicsState(); // this.internal.write('Q'); - } - })(); - } else { - this.saveGraphicsState(); // this.internal.write('q'); - try { - return cb(); - } finally { - this.restoreGraphicsState(); // this.internal.write('Q'); - } - } -} - -async function getImageFromURL(url) { - const response = await fetch(url); - const blob = await response.blob(); - - const dataURL = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener("load", () => { - resolve(reader.result); - }, false); - reader.readAsDataURL(blob); - }); - - const img = await new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - // img.src, - // img.height, - // img.width - resolve(img); - }; - img.src = dataURL; // could also just use the image file url here - }); - - return img; -} - const fire = ; const water = ; const earth = ; @@ -747,29 +574,12 @@ if (globalThis.__ubg_file_cache__) { globalThis.__ubg_file_cache__ = new FileCache(); } -const SORT_BY = { - ID: 'id', - NAME: 'name' -}; - const VP_FILL = 'violet'; const VP_STROKE = '#9400d3'; // darkviolet const VP_FILL_ZERO = '#ddd'; const VP_STROKE_ZERO = 'gray'; const AFFECT_ALL_COLOR = 'rgba(255, 0, 0, 0.2)'; -import 'src/external/dom-to-image.js' -const affectAllBackground = await (async function getAffectAllBackground() { - const div =
; - document.body.append(div) - try { - const dataUrl = await globalThis.domtoimage.toPng(div) - return `url(${dataUrl})`; - } finally { - div.remove(); - } -})() - class RuleTextRenderer { static parseEffectsAndLists(printedRules) { @@ -824,8 +634,9 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1 // separate rules printedRules = printedRules.replace(/affectAll(.*)\/affectAll/gmi, function replacer(match, innerText, offset, string, groups) { - return `
${innerText}
`; + return `
${innerText}
`; }); + printedRules = this.parseEffectsAndLists(printedRules); printedRules = this.renderReminderText(printedRules, cardEditor, cardDesc) @@ -890,7 +701,7 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1 static renderReminderText(printedRules, cardEditor, cardDesc) { function italic(text) { - return `${text}` + return `${text}` } return printedRules.replace(/\bremind(?:er)?(\w+(?:\-(\w|\(|\))*)*)\b/gmi, (match, myMatch, offset, string, groups) => { @@ -1302,12 +1113,12 @@ ${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 1 let textToPrint = this.__textOnIcon__(vp, rect, center); Math.sqrt(.5) - return ` -${SVG.inlineSVG(` - -${textToPrint} -`)} -`; + return `${SVG.inlineSVG(` + + + +${textToPrint} +`)}`; } 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) { @@ -1415,10 +1226,13 @@ margin: calc(${marginCalc}); padding: calc(${paddingCalc}); border: ${innerStrokeColor} solid ${innerStrokeWidth}mm; border-radius: 1mm; + font-size: 12pt; -font-family: "${CSS_FONT_FAMILY_UNIVERS_55}"; +font-family: "${CSS_FONT_FAMILY_CARD_TEXT}"; min-height: calc(${ruleTextBox.height}mm - 2 * (${paddingCalc})); + +backdrop-filter: blur(4px); `}>; ruleTextElement.innerHTML = printedRules; @@ -1428,659 +1242,189 @@ min-height: calc(${ruleTextBox.height}mm - 2 * (${paddingCalc})); const OUTSIDE_BORDER_ROUNDING = lively.pt(3, 3) -export class Cards extends Morph { - async initialize() { - - this.setAttribute("tabindex", 0); - this.windowTitle = "UBG Cards Viewer"; - this.addEventListener('contextmenu', evt => this.onMenuButton(evt), false); - - this.filter.value = this.filterValue; - this.rangeStart.value = this.rangeStartValue; - this.rangeEnd.value = this.rangeEndValue; - - this.updateView(); - lively.html.registerKeys(this); - this.registerButtons(); - - this.filter.addEventListener('keydown', evt => { - if (evt.key === 'Escape') { - this.filter.value = ''; - evt.stopPropagation(); - evt.preventDefault(); - // programmatic change does not emit an 'input' event, so we emit here explicitly - this.filter.dispatchEvent(new Event('input', { - bubbles: true, - cancelable: true - })); - } - }); - - for (let eventName of ['input']) { - this.filter.addEventListener(eventName, evt => this.filterChanged(evt), false); - this.rangeStart.addEventListener(eventName, evt => this.rangeChanged(evt), false); - this.rangeEnd.addEventListener(eventName, evt => this.rangeChanged(evt), false); - } - } - +export default class UbgCard extends Morph { /*MD ## Filter MD*/ - get filter() { - return this.get('#filter'); - } - get filterValue() { - return this.getAttribute('filter-Value') || ''; - } - set filterValue(value) { - this.setAttribute('filter-Value', value); + get assetsFolder() { + return this.src.replace(/(.*)\/.*$/i, '$1/assets/'); } - get rangeStart() { - return this.get('#rangeStart'); - } - get rangeStartValue() { - return this.getAttribute('rangeStart-Value') || ''; - } - set rangeStartValue(value) { - this.setAttribute('rangeStart-Value', value); - } - get rangeEnd() { - return this.get('#rangeEnd'); - } - get rangeEndValue() { - return this.getAttribute('rangeEnd-Value') || ''; - } - set rangeEndValue(value) { - this.setAttribute('rangeEnd-Value', value); + /*MD ## Build MD*/ + async fetchAssetsInfo() { + return (await this.assetsFolder.fetchStats()).contents; } - rangeChanged(evt) { - this.rangeStartValue = this.rangeStart.value; - this.rangeEndValue = this.rangeEnd.value; + /*MD ## Extract Card Info MD*/ + colorsForCard(card) { + const BOX_FILL_OPACITY = 0.7; + + const currentVersion = card.versions.last; - this.updateItemsToRange(); - this.updateSelectedItemToFilterAndRange(); - return; - } + if (card.getType() === 'character') { + return ['#efc241', '#b8942d', BOX_FILL_OPACITY]; + } - filterChanged(evt) { - this.filterValue = this.filter.value; + const multiElement = Array.isArray(card.getElement()); + if (multiElement) { + return ['#ff88ff', '#ff00ff', BOX_FILL_OPACITY]; + } - this.updateItemsToFilter(); - this.updateSelectedItemToFilterAndRange(); - return; - } + const singleElementColors = { + fire: ['#ffaaaa', '#dd0000', BOX_FILL_OPACITY], + water: ['#aaaaff', '#0000ff', BOX_FILL_OPACITY], + earth: ['#eeee88', '#cccc00', BOX_FILL_OPACITY], + wind: ['#88ff88', '#00bb00', BOX_FILL_OPACITY] + }[currentVersion.element && currentVersion.element.toLowerCase && currentVersion.element.toLowerCase()]; + if (singleElementColors) { + return singleElementColors; + } - updateItemsToRange() { - const start = this.rangeStartValue; - const end = this.rangeEndValue; - this.allEntries.forEach(entry => { - entry.updateToRange(start, end); - }); + return ['#ffffff', '#888888', BOX_FILL_OPACITY]; } - updateItemsToFilter() { - const filterValue = this.filterValue; - this.allEntries.forEach(entry => { - entry.updateToFilter(filterValue); - }); + getNameFromCard(cardDesc) { + const currentVersion = cardDesc.versions.last; + return currentVersion.name || '' } - - updateSelectedItemToFilterAndRange() { - const selectedEntry = this.selectedEntry; - if (selectedEntry) { - if (!selectedEntry.isVisible()) { - selectedEntry.classList.remove('selected'); - const downwards = this.findNextVisibleItem(selectedEntry, false, false); - if (downwards) { - this.selectEntry(downwards); - } else { - const upwards = this.findNextVisibleItem(selectedEntry, true, false); - if (upwards) { - this.selectEntry(upwards); - } - } - } else { - this.scrollSelectedItemIntoView(); - } + + getElementsFromCard(cardDesc, grayIfEmpty) { + const element = cardDesc.getElement(); + if (Array.isArray(element)) { + return element + } else if (element) { + return [element] } else { - const newItem = this.findNextVisibleItem(undefined, false, false); - if (newItem) { - this.selectEntry(newItem); - } + return grayIfEmpty ? ['gray'] : [] } } - scrollSelectedItemIntoView() { - const selectedEntry = this.selectedEntry; - if (!selectedEntry) { - return; - } - - selectedEntry.scrollIntoView({ - behavior: "auto", - block: "nearest", - inline: "nearest" - }); + /*MD ## Debugging MD*/ + debugPoint(pt, color = 'red') { + this.content.append(
); + } + + debugRect(rect, color = 'red') { + return this.roundedRect(rect, 'transparent', color, 1 / 3.7795275591, 0); } + + /*MD ## Rendering Helpers MD*/ + line(start, end, color, width) { + const startX = start.x; + const startY = start.y; + const endX = end.x; + const endY = end.y; - selectNextListItem(evt, prev) { - const listItems = this.allEntries; + const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); + const angle = Math.atan2(endY - startY, endX - startX) * (180 / Math.PI); - if (listItems.length <= 1) { - return; - } + const line = document.createElement('div'); + line.style.width = length + 'mm'; + line.style.transform = `rotate(${angle}deg) translateY(${-width / 2}mm)`; + line.style.position = 'absolute'; + line.style.top = startY + 'mm'; + line.style.left = startX + 'mm'; + line.style.height = width + 'mm'; + line.style.backgroundColor = color; + line.style.transformOrigin = 'top left'; - const selectedEntry = this.selectedEntry; - const newItem = this.findNextVisibleItem(selectedEntry, prev, !evt.repeat); - if (newItem && newItem !== selectedEntry) { - this.selectEntry(newItem); - } + this.content.append(line) } - findNextVisibleItem(referenceItem, prev, allowLooping) { - let listItems = this.allEntries; - if (listItems.length === 0) { - return; - } + roundedRect(rect, fill, stroke, strokeWidth, borderRadius) { + const element =
; - const firstPass = listItems.find((item, index) => index > referenceIndex && item.isVisible()); - if (firstPass) { - return firstPass; - } + this.content.append(element) + + return element; + } + + colorWithOpacity(color, opacity) { + return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)` + } - if (!allowLooping) { - return; + /*MD ## Background Images MD*/ + filePathForBackgroundImage(cardDesc, assetsInfo) { + const id = cardDesc.id; + const typeString = cardDesc.getType() && cardDesc.getType().toLowerCase && cardDesc.getType().toLowerCase() + const assetFileName = id + '.jpg'; + + if (id && assetsInfo.find(entry => entry.type === 'file' && entry.name === assetFileName)) { + return this.assetsFolder + assetFileName; } - return listItems.find((item, index) => index <= referenceIndex && item.isVisible()); + const defaultFiles = { + gadget: 'default-gadget.jpg', + character: 'default-character.jpg', + spell: 'default-spell.jpg' + }; + return this.assetsFolder + (defaultFiles[typeString] || 'default.jpg'); + } + + async setBackgroundImage(cardDesc, assetsInfo) { + const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo); + await this._setBackgroundImage(filePath) } - async onKeyDown(evt) { - if (evt.key === 'PageUp') { - evt.stopPropagation(); - evt.preventDefault(); - - this.selectNextEntryInDirection(true, !evt.repeat); - return; - } - - if (evt.key === 'PageDown') { - evt.stopPropagation(); - evt.preventDefault(); + async setBackgroundImageForCardBack() { + const filePath = this.assetsFolder + 'default-spell.jpg'; + await this._setBackgroundImage(filePath) + } - this.selectNextEntryInDirection(false, !evt.repeat); - return; - } + // #TODO: wait for image to be loaded + async _setBackgroundImage(filePath) { + this.get('#bg').style.backgroundImage = `url(${filePath})` + } - if (evt.ctrlKey && !evt.repeat && evt.key == "p") { - evt.stopPropagation(); - evt.preventDefault(); + /*MD ## Rendering MD*/ + async renderMagicStyle(cardDesc, outsideBorder, assetsInfo) { + const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - if (evt.altKey) { - this.onPrintChanges(evt) - } else { - this.onPrintSelected(evt) - } - return; - } + // black border + this.roundedRect(outsideBorder, 'black', 'transparent', '0', '0') - if (evt.ctrlKey && evt.key == "s") { - evt.stopPropagation(); - evt.preventDefault(); + // innerBorder + const INNER_INSET = 3; + const innerBorder = outsideBorder.insetBy(INNER_INSET); - if (!evt.repeat) { - await this.saveJSON(); - } else { - lively.warn('prevent saving multiple times'); - } - return; - } + // id + this.renderId(cardDesc) + this.get('#id-version').style.color = 'white' - if (evt.ctrlKey && evt.key == "i") { - evt.stopPropagation(); - evt.preventDefault(); - - if (!evt.repeat) { - await this.onImportNewCards(evt); - } else { - lively.warn('prevent importing multiple times'); - } - return; - } - - if (evt.ctrlKey && evt.key == "+") { - evt.stopPropagation(); - evt.preventDefault(); - - if (!evt.repeat) { - await this.addNewCard(); - } else { - lively.warn('prevent adding multiple new cards'); - } - return; - } - - if (evt.ctrlKey && evt.key == "Delete") { - evt.stopPropagation(); - evt.preventDefault(); - - if (!evt.repeat) { - await this.deleteCurrentEntry(); - } else { - lively.warn('prevent deleting multiple cards'); - } - return; - } - - if (evt.ctrlKey && !evt.repeat && evt.key == "/") { - evt.stopPropagation(); - evt.preventDefault(); - - if (this.filter.matches(':focus')) { - this.editor.focusOnText() - } else { - this.filter.select(); - } - return; - } - - // lively.notify(evt.key, evt.repeat); - } - - selectNextEntryInDirection(up, loop) { - const newEntry = this.findNextVisibleItem(this.selectedEntry, up, loop); - if (newEntry) { - this.selectEntry(newEntry); - } - } - - get src() { - return this.getAttribute("src"); - } - - set src(url) { - this.setAttribute("src", url); - // this.updateView(); - } - - get assetsFolder() { - return this.src.replace(/(.*)\/.*$/i, '$1/assets/'); - } - - async addCards(cards) { - for await (const card of cards) { - await this.addCard(card) - } - } - - async addCard(card) { - this.cards.push(card); - await this.appendCardEntry(card); - } - - async appendCardEntry(card) { - var entry = await identity(); - entry.value = card; - this.appendChild(entry); - return entry; - } - - async updateView() { - this.innerHTML = ""; - - if (!this.src) { - lively.warn('no src for ubg-cards'); - return; - } - - // debugger - if (!this.cards) { - this.cards = []; - try { - const cardsToLoad = await this.loadCardsFromFile(); - await this.addCards(cardsToLoad) - } catch (e) { - this.innerHTML = "" + e; - } - } else { - // ensure an entry for each card - const currentEntries = this.allEntries; - for (const card of this.cards) { - let entry = currentEntries.find(entry => entry.card === card); - if (!entry) { - entry = await this.appendCardEntry(card); - } - } - } - this.updateStats() - - this.selectCard(this.card || this.cards.first); - } - - updateStats() { - const stats = this.get('#stats'); - try { - stats.innerHTML = '' - - function lowerCase() { - return this && typeof this.toLowerCase === 'function' && this.toLowerCase(); - } - - const typeSplit = Object.entries(this.cards.groupBy(c => c.getType()::lowerCase())).map(([type, cards]) =>
{type}: {cards.length}
); - const elementSplit = Object.entries(this.cards.groupBy(c => c.getElement()::lowerCase())).map(([element, cards]) =>
{element}: {cards.length} ({cards.filter(c => c.getType()::lowerCase() === 'spell').length})
); - stats.append(
{...typeSplit}---{...elementSplit}
) - } catch (e) { - stats.append(
{e}
) - } - } - - get allEntries() { - return [...this.querySelectorAll('ubg-cards-entry')]; - } - - get selectedEntry() { - return this.allEntries.find(entry => entry.hasAttribute('selected')); - } - - selectEntry(entry) { - this.selectCard(entry.card); - } - - selectCard(card) { - this.card = card; - - this.allEntries.forEach(entry => { - if (entry.value === card) { - entry.setAttribute('selected', true); - } else { - entry.removeAttribute('selected'); - } - }); - - this.updateCardInEditor(card); - this.scrollSelectedItemIntoView(); - } - - updateCardInEditor(card) { - this.editor.src = card; - } - - entryForCard(card) { - return this.allEntries.find(entry => entry.card === card); - } - - async loadCardsFromFile() { - const text = await this.src.fetchText(); - const source = deserialize(text, { Card }); - // source.forEach(card => card.migrateTo(Card)) - return source; - } - - get viewerContainer() { - return this.get('#viewerContainer'); - } - - openInNewTab(doc) { - window.open(doc.output('bloburl'), '_blank'); - } - - async quicksavePDF(doc) { - doc.save('cards.pdf'); - } - - getAllTags() { - if (!this._allTags) { - const tagCount = new Map(); - this.cards.forEach(card => { - card.getTags().forEach(tag => { - tagCount.set(tag, (tagCount.get(tag) || 0) + 1); - }) - }) - this._allTags = [...tagCount.entries()].sortBy('second', false).map(pair => pair.first); - } - return this._allTags - } - - invalidateTags() { - delete this._allTags - } - - /*MD ## Build MD*/ - async ensureJSPDFLoaded() { - await lively.loadJavaScriptThroughDOM('jspdf', lively4url + '/src/external/jspdf/jspdf.umd.js'); - await lively.loadJavaScriptThroughDOM('svg2pdf', lively4url + '/src/external/jspdf/svg2pdf.umd.js'); - await lively.loadJavaScriptThroughDOM('html2canvas', lively4url + '/src/external/jspdf/html2canvas.js'); - } - - async createPDF(config) { - await this.ensureJSPDFLoaded(); - return new jspdf.jsPDF(config); - } - - async buildSingleCard(card) { - const doc = await this.createPDF({ - orientation: 'p', - unit: 'mm', - format: POKER_CARD_SIZE_MM.toPair - // putOnlyUsedFonts:true, - // floatPrecision: 16 // or "smart", default is 16 - () }); - - return this.buildCards(doc, [card], true); - } - - async buildFullPDF(cards) { - const doc = await this.createPDF({ - orientation: 'p', - unit: 'mm' - // format: POKER_CARD_SIZE_MM.addXY(5, 5).toPair(), - // putOnlyUsedFonts:true, - // floatPrecision: 16 // or "smart", default is 16 - }); - - return this.buildCards(doc, cards, false); // .slice(0,12) - } - - async fetchAssetsInfo() { - return (await this.assetsFolder.fetchStats()).contents; - } - - /*MD ### BUILD MD*/ - async buildCards(doc, cardsToPrint, skipCardBack) { - const GAP = lively.pt(.2, .2); - - const rowsPerPage = Math.max(((doc.internal.pageSize.getHeight() + GAP.y) / (POKER_CARD_SIZE_MM.y + GAP.y)).floor(), 1); - const cardsPerRow = Math.max(((doc.internal.pageSize.getWidth() + GAP.x) / (POKER_CARD_SIZE_MM.x + GAP.x)).floor(), 1); - const cardsPerPage = rowsPerPage * cardsPerRow; - - const margin = lively.pt(doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight()).subPt(lively.pt(cardsPerRow, rowsPerPage).scaleByPt(POKER_CARD_SIZE_MM).addPt(lively.pt(cardsPerRow - 1, rowsPerPage - 1).scaleByPt(GAP))); - - function progressLabel(numCard) { - return `process cards ${numCard}/${cardsToPrint.length}`; - } - const progress = await lively.showProgress(progressLabel(0)); - - if (!skipCardBack) { - doc.addPage("p", "mm", "a4"); - } - - try { - const assetsInfo = await this.fetchAssetsInfo(); - - let i = 0; - let currentPage = 0; - while (i < cardsToPrint.length) { - progress.value = (i + 1) / cardsToPrint.length; - progress.textContent = progressLabel(i); - - const indexOnPage = i % cardsPerPage; - const intendedPage = (i - indexOnPage) / cardsPerPage; - // lively.notify(`${i} ${indexOnPage} ${intendedPage}`); - if (currentPage < intendedPage) { - doc.addPage("p", "mm", "a4"); - doc.addPage("p", "mm", "a4"); - currentPage++; - lively.notify(currentPage) - } - const frontPage = 2 * currentPage + 1 - doc.setPage(frontPage) - - const rowIndex = (indexOnPage / rowsPerPage).floor(); - const columnIndex = indexOnPage % cardsPerRow; - const offset = lively.pt(columnIndex * (POKER_CARD_SIZE_MM.x + GAP.x), rowIndex * (POKER_CARD_SIZE_MM.y + GAP.y)).addPt(margin.scaleBy(1 / 2)); - const outsideBorder = offset.extent(POKER_CARD_SIZE_MM); - - // a.æÆ() - const cardToPrint = cardsToPrint[i]; - await this.renderCard(doc, cardToPrint, outsideBorder, assetsInfo); - - if (!skipCardBack) { - const backPage = frontPage + 1 - doc.setPage(backPage) - - const rowIndex = (indexOnPage / rowsPerPage).floor(); - const columnIndex = cardsPerRow - 1 - indexOnPage % cardsPerRow; - const offset = lively.pt(columnIndex * (POKER_CARD_SIZE_MM.x + GAP.x), rowIndex * (POKER_CARD_SIZE_MM.y + GAP.y)).addPt(margin.scaleBy(1 / 2)); - const outsideBorder = offset.extent(POKER_CARD_SIZE_MM); - - // a.æÆ() - const cardToPrint = cardsToPrint[i]; - await this._renderCardBack(doc, cardToPrint, outsideBorder, assetsInfo); - } - - i++; - } - } finally { - progress.remove(); - } - - return doc; - } - - get editor() { - return this.get('#editor'); - } - - getCharacterColors() { - } - - colorsForCard(card) { - const BOX_FILL_OPACITY = 0.7; - - const currentVersion = card.versions.last; - - if (card.getType() === 'character') { - return ['#efc241', '#b8942d', BOX_FILL_OPACITY]; - } - - const multiElement = Array.isArray(card.getElement()); - if (multiElement) { - return ['#ff88ff', '#ff00ff', BOX_FILL_OPACITY]; - } - - const singleElementColors = { - fire: ['#ffaaaa', '#dd0000', BOX_FILL_OPACITY], - water: ['#aaaaff', '#0000ff', BOX_FILL_OPACITY], - earth: ['#eeee88', '#cccc00', BOX_FILL_OPACITY], - wind: ['#88ff88', '#00bb00', BOX_FILL_OPACITY] - }[currentVersion.element && currentVersion.element.toLowerCase && currentVersion.element.toLowerCase()]; - if (singleElementColors) { - return singleElementColors; - } - - return ['#ffffff', '#888888', BOX_FILL_OPACITY]; - } - - /*MD ## Extract Card Info MD*/ - getNameFromCard(cardDesc) { - const currentVersion = cardDesc.versions.last; - return currentVersion.name || '' - } - - getElementsFromCard(cardDesc, grayIfEmpty) { - const element = cardDesc.getElement(); - if (Array.isArray(element)) { - return element - } else if (element) { - return [element] - } else { - return grayIfEmpty ? ['gray'] : [] - } - } - getRulesTextFromCard(cardDesc) { - - } - - /*MD ## Rendering MD*/ - async renderCard(doc, cardDesc, outsideBorder, assetsInfo) { - return await this.renderFullBleedStyle(doc, cardDesc, outsideBorder, assetsInfo) - } - - async getBackgroundImage(doc, cardDesc, bounds, assetsInfo) { - const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo); - return await this.loadBackgroundImageForFile(filePath, bounds) - } - - filePathForBackgroundImage(cardDesc, assetsInfo) { - const id = cardDesc.id; - const typeString = cardDesc.getType() && cardDesc.getType().toLowerCase && cardDesc.getType().toLowerCase() - const assetFileName = id + '.jpg'; - - if (id && assetsInfo.find(entry => entry.type === 'file' && entry.name === assetFileName)) { - return this.assetsFolder + assetFileName; - } - - const defaultFiles = { - gadget: 'default-gadget.jpg', - character: 'default-character.jpg', - spell: 'default-spell.jpg' - }; - return this.assetsFolder + (defaultFiles[typeString] || 'default.jpg'); - } - - async loadBackgroundImageForFile(filePath, bounds) { - const img = await globalThis.__ubg_file_cache__.getFile(filePath, getImageFromURL); - const imgRect = lively.rect(0, 0, img.width, img.height); - const scaledRect = imgRect.fitToBounds(bounds, true); - - return { img, scaledRect } - } - - async renderMagicStyle(doc, cardDesc, outsideBorder, assetsInfo) { - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - - // black border - this.roundedRect(outsideBorder, 'black', 'transparent', '0', '0') - - // innerBorder - const INNER_INSET = 3; - const innerBorder = outsideBorder.insetBy(INNER_INSET); - - // id - this.renderId(cardDesc) - this.get('#id-version').style.color = 'white' - - // card image - const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo); - const newBG =
; - this.content.append(newBG) + // card image + const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo); + const newBG =
; + this.content.append(newBG) // title bar const TITLE_BAR_HEIGHT = 7; @@ -2110,18 +1454,18 @@ font-family: "${CSS_FONT_FAMILY_CARD_NAME}"; // cost const COIN_RADIUS = 4; const coinPos = titleBar.bottomLeft().addY(1).addXY(COIN_RADIUS, COIN_RADIUS); - this.renderCost(doc, cardDesc, coinPos, COIN_RADIUS) + this.renderCost(cardDesc, coinPos, COIN_RADIUS) // type & elements const typePos = coinPos.addY(COIN_RADIUS * 1.5) - await this.renderType(cardDesc, typePos, BOX_FILL_COLOR, BOX_FILL_OPACITY) + this.renderType(cardDesc, typePos, BOX_FILL_COLOR, BOX_FILL_OPACITY) // rule box const ruleBox = outsideBorder.copy() const height = outsideBorder.height * .4; ruleBox.y = ruleBox.bottom() - height; ruleBox.height = height; - this.debugRect(ruleBox) + // this.debugRect(ruleBox) const ruleBoxInset = 1 + INNER_INSET; const ruleTextInset = 2; await this.renderRuleText(cardDesc, outsideBorder, ruleBox, { @@ -2137,21 +1481,21 @@ font-family: "${CSS_FONT_FAMILY_CARD_NAME}"; // tags const tagsAnchor = titleBar.bottomRight().addY(1); - await this.renderTags(cardDesc, tagsAnchor, outsideBorder) + this.renderTags(cardDesc, tagsAnchor, outsideBorder) } - async renderFullBleedStyle(doc, cardDesc, outsideBorder, assetsInfo) { + async renderFullBleedStyle(cardDesc, outsideBorder, assetsInfo) { const type = cardDesc.getType(); const typeString = type && type.toLowerCase && type.toLowerCase() || ''; if (typeString === 'spell') { - await this.renderSpell(doc, cardDesc, outsideBorder, assetsInfo) + await this.renderSpell(cardDesc, outsideBorder, assetsInfo) } else if (typeString === 'gadget') { - await this.renderGadget(doc, cardDesc, outsideBorder, assetsInfo) + await this.renderGadget(cardDesc, outsideBorder, assetsInfo) } else if (typeString === 'character') { - await this.renderCharacter(doc, cardDesc, outsideBorder, assetsInfo) + await this.renderCharacter(cardDesc, outsideBorder, assetsInfo) } else { - await this.renderMagicStyle(doc, cardDesc, outsideBorder, assetsInfo) + await this.renderMagicStyle(cardDesc, outsideBorder, assetsInfo) } this.renderIsBad(cardDesc, outsideBorder) @@ -2179,11 +1523,11 @@ position: absolute; } /*MD ### Rendering Card Types MD*/ // #important - async renderSpell(doc, cardDesc, outsideBorder, assetsInfo) { + async renderSpell(cardDesc, outsideBorder, assetsInfo) { const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); // background card image - this.setBackgroundImage(cardDesc, assetsInfo) + await this.setBackgroundImage(cardDesc, assetsInfo) // spell circle { @@ -2205,14 +1549,14 @@ position: absolute; const titleBorder = innerBorder.insetBy(1); titleBorder.height = TITLE_BAR_HEIGHT; - await this.renderTitleBarAndCost(doc, cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) + this.renderTitleBarAndCost(cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) // rule box const ruleBox = outsideBorder.copy() const height = outsideBorder.height * .3; ruleBox.y = ruleBox.bottom() - height; ruleBox.height = height; - this.debugRect(ruleBox) + // this.debugRect(ruleBox) // rule text const RULE_BOX_INSET = 1; @@ -2227,76 +1571,18 @@ position: absolute; // tags const tagsAnchor = lively.pt(titleBorder.right(), titleBorder.bottom()).addXY(-RULE_TEXT_INSET, 1); - await this.renderTags(cardDesc, tagsAnchor, outsideBorder) + this.renderTags(cardDesc, tagsAnchor, outsideBorder) // id this.renderId(cardDesc) } - line(start, end, color, width) { - const startX = start.x; - const startY = start.y; - const endX = end.x; - const endY = end.y; - - const length = Math.sqrt(Math.pow(endX - startX, 2) + Math.pow(endY - startY, 2)); - const angle = Math.atan2(endY - startY, endX - startX) * (180 / Math.PI); - - const line = document.createElement('div'); - line.style.width = length + 'mm'; - line.style.transform = `rotate(${angle}deg) translateY(${-width / 2}mm)`; - line.style.position = 'absolute'; - line.style.top = startY + 'mm'; - line.style.left = startX + 'mm'; - line.style.height = width + 'mm'; - line.style.backgroundColor = color; - line.style.transformOrigin = 'top left'; - - this.content.append(line) - } - - roundedRect(rect, fill, stroke, strokeWidth, borderRadius) { - const element =
; - this.content.append(element) - } - - colorWithOpacity(color, opacity) { - return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)` - } - - setBackgroundImage(cardDesc, assetsInfo) { - const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo); - this.setCBImage(filePath) - } - - setBackgroundImageForCardBack() { - const filePath = this.assetsFolder + 'default-spell.jpg'; - this.setCBImage(filePath) - } - - setCBImage(filePath) { - this.get('#bg').style.backgroundImage = `url(${filePath})` - } - // #important - async renderGadget(doc, cardDesc, outsideBorder, assetsInfo) { + async renderGadget(cardDesc, outsideBorder, assetsInfo) { const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); // background card image - this.setBackgroundImage(cardDesc, assetsInfo) + await this.setBackgroundImage(cardDesc, assetsInfo) // innerBorder const innerBorder = outsideBorder.insetBy(3); @@ -2306,7 +1592,8 @@ position: absolute; const topBox = outsideBorder.copy() { topBox.height = 13; - this.roundedRect(topBox, this.colorWithOpacity(BOX_FILL_COLOR, BOX_FILL_OPACITY), 'transparent', 0, 0); + const box = this.roundedRect(topBox, this.colorWithOpacity(BOX_FILL_COLOR, BOX_FILL_OPACITY), 'transparent', 0, 0); + box.style.backdropFilter = 'blur(4px)'; this.line(topBox.bottomLeft(), topBox.bottomRight(), BOX_STROKE_COLOR, 1) } @@ -2320,7 +1607,7 @@ position: absolute; const titleBorder = innerBorder.insetBy(1); titleBorder.height = TITLE_BAR_HEIGHT; - await this.renderTitleBarAndCost(doc, cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) + this.renderTitleBarAndCost(cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) } // rule box border calc @@ -2328,7 +1615,7 @@ position: absolute; const height = outsideBorder.height * .4; ruleBox.y = ruleBox.bottom() - height; ruleBox.height = height; - this.debugRect(ruleBox) + // this.debugRect(ruleBox) // rule text const RULE_BOX_INSET = 1; @@ -2343,18 +1630,18 @@ position: absolute; // tags const tagsAnchor = lively.pt(topBox.right(), topBox.bottom()).addXY(-RULE_TEXT_INSET, 1); - await this.renderTags(cardDesc, tagsAnchor, outsideBorder) + this.renderTags(cardDesc, tagsAnchor, outsideBorder) // id this.renderId(cardDesc) } // #important - async renderCharacter(doc, cardDesc, outsideBorder, assetsInfo) { + async renderCharacter(cardDesc, outsideBorder, assetsInfo) { const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); // background card image - this.setBackgroundImage(cardDesc, assetsInfo) + await this.setBackgroundImage(cardDesc, assetsInfo) // Zohar design { @@ -2392,7 +1679,7 @@ position: absolute; const titleBorder = innerBorder.insetBy(1); titleBorder.height = TITLE_BAR_HEIGHT; - await this.renderTitleBarAndCost(doc, cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) + this.renderTitleBarAndCost(cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) // rule box border calc const ruleBox = outsideBorder.copy() @@ -2413,33 +1700,14 @@ position: absolute; // tags const tagsAnchor = lively.pt(titleBorder.right(), titleBorder.bottom()).addXY(-RULE_TEXT_INSET, 1); - await this.renderTags(cardDesc, tagsAnchor, outsideBorder) + this.renderTags(cardDesc, tagsAnchor, outsideBorder) // id this.renderId(cardDesc) } /*MD ### Rendering Card Components MD*/ - withinCardBorder(doc, outsideBorder, cb) { - function clipOuterBorder() { - doc.roundedRect(...outsideBorder::xYWidthHeight(), OUTSIDE_BORDER_ROUNDING.x, OUTSIDE_BORDER_ROUNDING.y, null); // set clipping area - doc.internal.write('W n'); - } - - if (isAsync(cb)) { - return doc::withGraphicsState(async () => { - clipOuterBorder() - return await cb(); - }); - } else { - return doc::withGraphicsState(() => { - clipOuterBorder() - return cb(); - }); - } - } - - async renderTitleBarAndCost(doc, cardDesc, border, costCoinRadius, costCoinMargin) { + renderTitleBarAndCost(cardDesc, border, costCoinRadius, costCoinMargin) { const TITLE_BAR_BORDER_WIDTH = 0.200025; const titleBar = border.copy() @@ -2473,19 +1741,19 @@ font-family: "${CSS_FONT_FAMILY_CARD_NAME}"; } const coinCenter = coinLeftCenter.addX(costCoinRadius); - await this.renderInHandSymbols(doc, cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) + this.renderInHandSymbols(cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) } - async renderInHandSymbols(doc, cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) { + renderInHandSymbols(cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) { let currentCenter = coinCenter; // cost - await this.renderCost(doc, cardDesc, currentCenter, costCoinRadius) + this.renderCost(cardDesc, currentCenter, costCoinRadius) if ((cardDesc.getType() || '').toLowerCase() !== 'character') { // vp currentCenter = currentCenter.addY(costCoinRadius * 2.75); - await this.renderBaseVP(doc, cardDesc, currentCenter, costCoinRadius) + this.renderBaseVP(cardDesc, currentCenter, costCoinRadius) // element (list) currentCenter = currentCenter.addY(costCoinRadius * 2.75); @@ -2498,7 +1766,7 @@ font-family: "${CSS_FONT_FAMILY_CARD_NAME}"; // type currentCenter = currentCenter.addY(costCoinRadius * .75) const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - await this.renderType(cardDesc, currentCenter, BOX_FILL_COLOR, BOX_FILL_OPACITY) + this.renderType(cardDesc, currentCenter, BOX_FILL_COLOR, BOX_FILL_OPACITY) } renderElementList(cardDesc, pos, radius, direction) { @@ -2510,7 +1778,7 @@ font-family: "${CSS_FONT_FAMILY_CARD_NAME}"; return pos.addY(direction * radius * .25); } - async renderCost(doc, cardDesc, pos, coinRadius) { + renderCost(cardDesc, pos, coinRadius) { const costSize = coinRadius / 3; const costDesc = cardDesc.getCost(); @@ -2538,10 +1806,10 @@ left: ${coinCenter.x - coinRadius}mm; // this.content.append(str); // } - await this.renderIconText(doc, coinCenter, costSize, cost, CSS_FONT_FAMILY_CARD_COST) + this.renderIconText(coinCenter, costSize, cost, CSS_FONT_FAMILY_CARD_COST) } - async renderBaseVP(doc, cardDesc, pos, coinRadius) { + renderBaseVP(cardDesc, pos, coinRadius) { const costSize = coinRadius / 3; const vp = cardDesc.getBaseVP() || 0; @@ -2566,16 +1834,15 @@ position: absolute; ; this.content.insertAdjacentHTML('beforeend', svg.outerHTML) - - await this.renderIconText(doc, iconCenter, costSize, vp, CSS_FONT_FAMILY_CARD_VP) + this.renderIconText(iconCenter, costSize, vp, CSS_FONT_FAMILY_CARD_VP) } - async renderIconText(doc, centerPos, size, text, font) { + renderIconText(centerPos, size, text, font) { if (text === undefined) { return } - this.content.append({'' + text}) +`}>{'' + text}; + + this.content.append(iconText) } // #important @@ -2591,25 +1860,7 @@ font-family: "${font}"; return RuleTextRenderer.renderRuleText(this, cardDesc, outsideBorder, ruleBox, options) } - debugPoint(pt, color = 'red') { - this.content.append(
); - } - - debugRect(rect, color = 'red') { - return this.roundedRect(rect, 'transparent', color, 1 / 3.7795275591, 0); - } - - // type - async renderType(cardDesc, anchorPt, color, opacity) { + renderType(cardDesc, anchorPt, color, opacity) { // function curate() { // return this.toLower().upperFirst(); // } @@ -2707,11 +1958,11 @@ font-family: ${CSS_FONT_FAMILY_UNIVERS_55}; this.get('#version-indicator').style.setProperty("--version-fill", VERSION_FILL); } - async _renderCardBack(doc, cardDesc, outsideBorder, assetsInfo) { + async _renderCardBack(cardDesc, outsideBorder, assetsInfo) { const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); // background card image - this.setBackgroundImageForCardBack() + await this.setBackgroundImageForCardBack() // inner border { @@ -2736,491 +1987,9 @@ font-family: ${CSS_FONT_FAMILY_UNIVERS_55}; this.get('#id-version').remove() this.get('#version-indicator').remove() } - - /*MD ## Preview MD*/ - async loadPDFFromURLToBase64(url) { - // Loading document - // Load a blob, transform the blob into base64 - // Base64 is the format we need since it is editable and can be shown by PDFJS at the same time. - const response = await fetch(url); - const blob = await response.blob(); - // let fileReader = new FileReader(); - // fileReader.addEventListener('loadend', () => { - // // this.editedPdfText = atob(fileReader.result.replace("data:application/pdf;base64,", "")); - // }); - - // fileReader.readAsDataURL(blob); - return URL.createObjectURL(blob); - } - - async showPDFData(base64pdf, container, viewer, context = 'ubg-cards-preview') { - { - const old = this.pdfContext && this.pdfContext[context]; - if (old) { - old.pdfDocument.destroy(); - old.pdfViewer.cleanup(); - this.pdfContext[context] = undefined; - } - } - - const eventBus = new window.PDFJSViewer.EventBus(); - const pdfLinkService = new window.PDFJSViewer.PDFLinkService({ eventBus }); - const pdfViewer = new window.PDFJSViewer.PDFViewer({ - eventBus, - container, - viewer, - linkService: pdfLinkService, - renderer: "canvas", // svg canvas - textLayerMode: 1 - }); - pdfLinkService.setViewer(pdfViewer); - container.addEventListener('pagesinit', () => { - pdfViewer.currentScaleValue = 1; - }); - - const pdfDocument = await PDFJS.getDocument(base64pdf).promise; - pdfViewer.setDocument(pdfDocument); - pdfLinkService.setDocument(pdfDocument, null); - - (this.pdfContext = this.pdfContext || {})[context] = { - eventBus, - pdfLinkService, - pdfViewer, - pdfDocument - }; - - await pdfViewer.pagesPromise; - // #TODO can we advice the pdfView to only render the current page we need? - // if (this.getAttribute("mode") != "scroll") { - // this.currentPage = 1 - // this.showPage(this.getPage(this.currentPage)) - // } - } - - /*MD ## --- MD*/ - // toBibtex() { - // var bibtex = ""; - // for (var ea of this.querySelectorAll("lively-bibtex-entry")) { - // bibtex += ea.innerHTML; - // } - // return bibtex; - // } - - /*MD ## Sorting MD*/ - get sortBy() { - return this.getAttribute('sortBy') || SORT_BY.ID; - } - - set sortBy(key) { - this.setAttribute('sortBy', key); - } - - get sortDescending() { - return this.hasAttribute('sort-descending'); - } - - set sortDescending(bool) { - if (bool) { - this.setAttribute('sort-descending', 'true'); - } else { - this.removeAttribute('sort-descending'); - } - } - - setSortKeyOrFlipOrder(key) { - if (this.sortBy === key) { - this.sortDescending = !this.sortDescending; - } else { - this.setAttribute('sortBy', key); - } - } - - sortEntries() { - const sortingFunction = this.getSortingFunction(); - const ascending = !this.sortDescending; - const sortedEntries = this.allEntries.sortBy(sortingFunction, ascending); - sortedEntries.forEach(entry => this.append(entry)); - } - - getSortingFunction() { - return { - id(entry) { - return entry.card.getId(); - }, - name(entry) { - return entry.card.getName(); - } - }[this.sortBy]; - } - - /*MD ## Main Bar Buttons MD*/ - onSortById(evt) { - this.setSortKeyOrFlipOrder(SORT_BY.ID); - this.sortEntries(); - } - - onSortByName(evt) { - this.setSortKeyOrFlipOrder(SORT_BY.NAME); - this.sortEntries(); - } - - - async onCopyIDs(evt) { - var begin = this.cards.maxProp('id') + 1 - const numIds = 100; - const idsText = numIds.times(i => begin + i).join('\n') - - try { - await this.copyTextToClipboard(idsText); - lively.success('copied ids for google docs!'); - } catch (e) { - shake(this.get('#copyIDs')); - lively.error('copying failed', e.message); - } - } - - async onImportNewCards(evt) { - lively.notify('onImportNewCards' + evt.shiftKey); - if (that && that.localName === 'lively-code-mirror' && document.contains(that)) { - lively.showElement(that) - - const matches = that.value.matchAll(/^([^0-9]+)?\s([0-9]+)?\s?([a-zA-Z ]+)?\s?(?:\(([0-9,]+)\))?(?:\s?([0-9*+-]+))?\.\s(.*)?$/gmi); - - const newCards = [...matches].map(match => { - const card = new Card(); - - const id = match[2]; - const intId = parseInt(id); - if (!_.isNaN(intId)) { - card.setId(intId) - } else { - card.setId(id) - } - - card.setName(match[1]) - card.setText(match[6]) - - const typesAndElements = match[3]; - if (typesAndElements) { - let type = '' - let element; - const typeElement = match[3].split(' ').forEach(te => { - if (!te) { - return; - } - - if (['gadget', 'character', 'spell'].includes(te.toLowerCase())) { - type += te - return - } - - if (!element) { - element = te - } else if (Array.isArray(element)) { - element.push(te) - } else { - element = [element, te] - } - }) - - if (type) { - card.setType(type) - } - - if (element) { - card.setElement(element) - } - } - const cost = match[4]; - const intCost = parseInt(cost); - if (!_.isNaN(intCost)) { - card.setCost(intCost) - } else { - if (cost) { - card.setCost(cost) - } - } - - const baseVP = match[5]; - const intBaseVP = parseInt(baseVP); - if (!_.isNaN(intBaseVP)) { - card.setBaseVP(intBaseVP) - } else { - if (baseVP) { - card.setBaseVP(baseVP) - } - } - - return card; - }); - - const doImport = await lively.confirm(`Import ${newCards.length} cards?
${newCards.map(c => c.getName()).join(', ')}`); - if (doImport) { - await this.addCards(newCards) - this.selectCard(newCards.last); - - this.markAsChanged(); - } - } else { - const workspace = await lively.openWorkspace("", lively.getPosition(evt)) - await workspace.editorLoaded() - that = workspace - workspace.value = 'paste card info here, then press import again' - workspace.editor.execCommand('selectAll'); - lively.showElement(workspace) - } - } - - async onArtDesc(evt) { - const text = do { - const assetsInfo = await this.fetchAssetsInfo(); - let ids = [] - for (let entry of assetsInfo) { - if (entry.type !== 'file') { - continue - } - - const match = entry.name.match(/^(.+)\.jpg$/) - if (!match) { - continue - } - - // const id = parseInt(match[1]) - // if (_.isInteger(id)) { - // ids.push(id) - // } - ids.push(match[1]) - } - - let cards = this.cards; - cards = cards - .filter(c => !c.hasTag('bad')) - // we just use a string match for now - .filter(c => !ids.includes(c.getId() + '')).sortBy('id') - cards.map(c => { - const artDesc = c.getArtDirection() || c.getName(); - return `[${c.getId()}, '${artDesc}'],` - }).join('\n') - }; - - try { - await this.copyTextToClipboard(text) - lively.success('copied art description!'); - } catch (e) { - shake(this.get('#artDesc')); - lively.error('copying failed', e.message); - } - } - - async copyTextToClipboard(text) { - const type = "text/plain"; - const blob = new Blob([text], { type }); - // evt.clipboardData.setData('text/html', html); - const data = [new ClipboardItem({ [type]: blob })]; - - return await navigator.clipboard.write(data); - } - - filterCardsForPrinting(cards) { - return cards.filter(card => { - if (card.getRating() === 'remove') { - return false; - } - - if (card.hasTag('duplicate')) { - return false; - } - if (card.hasTag('unfinished')) { - return false; - } - if (card.hasTag('bad')) { - return false; - } - if (card.hasTag('deprecated')) { - return false; - } - - return true - }) - } - - async onPrintSelected(evt) { - if (!this.cards) { - return; - } - - const filteredEntries = this.allEntries.filter(entry => entry.isVisible()) - const cardsToPrint = this.filterCardsForPrinting(filteredEntries.map(entry => entry.card)) - - if (await this.checkForLargePrinting(cardsToPrint)) { - await this.printForExport(cardsToPrint, evt.shiftKey); - } - } - - async onPrintChanges(evt) { - if (!this.cards) { - return; - } - - const cardsToPrint = this.filterCardsForPrinting(this.cards.filter(card => !card.getIsPrinted())); - - if (await this.checkForLargePrinting(cardsToPrint)) { - await this.printForExport(cardsToPrint, evt.shiftKey); - } - } - - async checkForLargePrinting(cardsToPrint) { - if (cardsToPrint.length > 30) { - return await lively.confirm(`Print ${cardsToPrint.length} cards?
${cardsToPrint.slice(0, 30).map(c => c.getName()).join(', ')}, ...`); - } - - return true; - } - - async printForExport(cards, quickSavePDF) { - if (cards.length === 0) { - lively.warn('no cards to print for export'); - return; - } - - // mark newly printed cards as printed - let anyNewlyPrintedCard = false - cards.forEach(card => { - if (card.getIsPrinted()) { - return - } - anyNewlyPrintedCard = true - card.setIsPrinted(true) - }) - if (anyNewlyPrintedCard) { - this.markAsChanged() - } - - const doc = await this.buildFullPDF(cards); - if (quickSavePDF) { - this.quicksavePDF(doc); - } else { - this.openInNewTab(doc); - } - } - - async saveJSON() { - lively.warn(`save ${this.src}`); - await lively.files.saveFile(this.src, serialize(this.cards)); - lively.success(`saved`); - this.clearMarkAsChanged(); - } - - async onSavePdf(evt) { - const pdfUrl = this.src.replace(/\.json$/, '.pdf'); - - if (!await lively.confirm(`Save full cards as ${pdfUrl}?`)) { - return; - } - - const cardsToSave = this.cards.slice(0, 12); - const doc = await this.buildFullPDF(cardsToSave); - const blob = doc.output('blob'); - await lively.files.saveFile(pdfUrl, blob); - } - - async onShowPreview(evt) { - const cardsToPreview = this.cards.slice(0, 12); - const doc = await this.buildFullPDF(cardsToPreview); - this.classList.add('show-preview'); - await this.showPDFData(doc.output('dataurlstring'), this.viewerContainer); - } - - onClosePreview(evt) { - this.classList.remove('show-preview'); - } - - async onAddButton(evt) { - await this.addNewCard(); - } - - async addNewCard() { - const highestId = this.cards.maxProp(card => card.getId()); - const newCard = new Card(); - newCard.setId(highestId + 1); - - await this.addCard(newCard) - this.selectCard(newCard); - - this.markAsChanged(); - } - - async onDeleteButton(evt) { - await this.deleteCurrentEntry(); - } - - async deleteCurrentEntry() { - const cardToDelete = this.card; - const entryToDelete = this.entryForCard(cardToDelete); - - await this.selectNextEntryInDirection(false, true); - - this.cards.removeItem(cardToDelete); - entryToDelete.remove(); - - this.markAsChanged(); - } - - async onMenuButton(evt) { - if (!evt.shiftKey) { - evt.stopPropagation(); - evt.preventDefault(); - - const menu = new ContextMenu(this, [ - ["foo", () => { - lively.notify(123) - }], ["bar", () => { - lively.notify(456) - }] - ]); - menu.openIn(document.body, evt, this); - return; - } - } - - /*MD ## change indicator MD*/ - get textChanged() { - return this.hasAttribute('text-changed'); - } - - markAsChanged() { - this.setAttribute('text-changed', true); - } - - markCardAsChanged(card) { - const entryToUpdate = this.entryForCard(card); - if (entryToUpdate) { - entryToUpdate.updateView(); - } - - this.markAsChanged(); - } - - clearMarkAsChanged() { - this.removeAttribute('text-changed'); - } - - /*MD ## lively API MD*/ - livelyMigrate(other) { - this.cards = other.cards; - this.card = other.card; - } - - livelySource() { - return Array.from(this.querySelectorAll("lively-bibtex-entry")).map(ea => ea.textContent).join(""); - } - -} - - -export default class UbgCard extends Cards { - async initialize() { + /*MD ## Basic Web Components MD*/ + initialize() { if (this.hasAttribute('for-preload')) { return; } @@ -3232,34 +2001,51 @@ export default class UbgCard extends Cards { return ["card", "src", "is-cardback"]; } - getCard() { - return this.card; + attributeChangedCallback(name, oldValue, newValue) { + lively.notify(`${oldValue} -> ${newValue}`, name) + } + + /*MD ## External API MD*/ + setSrc(src) { + return this.src = src; } setCard(card) { return this.card = card; } + setCards(cards) { + return this.cards = cards; + } + async render() { - const doc = undefined; + this._checkOptionsSet() const assetsInfo = await this.fetchAssetsInfo(); const outsideBorder = lively.pt(0,0).extent(POKER_CARD_SIZE_MM); const cardToPrint = this.card; - await this.renderCard(doc, cardToPrint, outsideBorder, assetsInfo); + await this.renderFullBleedStyle(cardToPrint, outsideBorder, assetsInfo) } async renderCardBack() { - const doc = undefined; + this._checkOptionsSet() const assetsInfo = await this.fetchAssetsInfo(); const outsideBorder = lively.pt(0,0).extent(POKER_CARD_SIZE_MM); const cardToPrint = this.card; - await this._renderCardBack(doc, cardToPrint, outsideBorder, assetsInfo) + await this._renderCardBack(cardToPrint, outsideBorder, assetsInfo) } - attributeChangedCallback(name, oldValue, newValue) { - lively.notify(`${oldValue} -> ${newValue}`, name) + _checkOptionsSet() { + if (!this.src) { + lively.warn('cannot render: "src" not set') + } + if (!this.card) { + lively.warn('cannot render: "card" not set') + } + if (!this.cards) { + lively.warn('cannot render: "cards" not set') + } } - + /*MD ## Lively-specific API MD*/ livelyPrepareSave() { // this.setAttribute("data-mydata", this.get("#textField").value) @@ -3270,7 +2056,16 @@ export default class UbgCard extends Cards { } livelyMigrate(other) { - this.isCardback = other.isCardback; + const src = other.src; + if (src) { + this.setSrc(src); + } + + const cards = other.cards; + if (cards) { + this.setCards(cards); + } + const card = other.card; if (card) { this.setCard(card); diff --git a/src/components/widgets/ubg-cards-editor.html b/src/components/widgets/ubg-cards-editor.html index 1ac7d703a..8edfe3aa2 100644 --- a/src/components/widgets/ubg-cards-editor.html +++ b/src/components/widgets/ubg-cards-editor.html @@ -127,21 +127,22 @@ #form-layout { display: grid; - grid-template-columns: min-content auto 2.5in 2.5in; - grid-template-rows: repeat(7, auto) 1fr auto auto .33fr auto; + grid-template-columns: min-content auto 2.5in; + grid-template-rows: repeat(8, auto) 1fr auto auto .33fr auto; grid-template-areas: - "isPrinted-key isPrinted-value preview preview2" - "id-key id-value preview preview2" - "name-key name-value preview preview2" - "type-key type-value preview preview2" - "element-key element-value preview preview2" - "cost-key cost-value preview preview2" - "vp-key vp-value preview preview2" - "text-key text-value preview preview2" - "tags-key tags-value preview preview2" - "rating-key rating-value preview preview2" - "notes-key notes-value preview preview2" - "art-key art-value preview preview2" + "isPrinted-key isPrinted-value preview" + "id-key id-value preview" + "name-key name-value preview" + "identity-key identity-value preview" + "type-key type-value preview" + "element-key element-value preview" + "cost-key cost-value preview" + "vp-key vp-value preview" + "text-key text-value preview" + "tags-key tags-value preview" + "rating-key rating-value preview" + "notes-key notes-value preview" + "art-key art-value preview" ; column-gap: 2px; row-gap: 2px; @@ -156,17 +157,6 @@ width: 2.5in; height: 3.5in; - background: steelblue; - overflow: hidden; - display: grid; - } - #preview-container2 { - position: relative; - grid-area: preview2; - - width: 2.5in; - height: 3.5in; -/* background: steelblue; */ overflow: hidden; display: grid; } @@ -176,19 +166,6 @@ right: 0; bottom: 0; left: 0; - background: green; - } - #preview2 { - position: absolute; - top: 0; -/* right: 0; */ -/* bottom: 0; */ - left: 0; -/* background: green; */ - } - #previewViewer { - display: hidden; - background: green; } .key { align-self: center; @@ -212,7 +189,7 @@ .textLayer { display: none; } - #preview-spinner, #preview-spinner2 { + #preview-spinner { color: lightgray; align-self: center; justify-self: center; @@ -222,9 +199,6 @@ :host([preview-queued]) #preview-spinner { display: block; } - :host([preview-queued]) #preview-spinner2 { - display: block; - } #tags-input { width: 150px; } @@ -252,6 +226,8 @@ name + identity + type element @@ -289,14 +265,8 @@
-
-
-
+
just a stand-in
-
-
just a stand-in
-
-
diff --git a/src/components/widgets/ubg-cards-editor.js b/src/components/widgets/ubg-cards-editor.js index 6f7e076f8..ec6b58308 100644 --- a/src/components/widgets/ubg-cards-editor.js +++ b/src/components/widgets/ubg-cards-editor.js @@ -18,6 +18,7 @@ export default class UBGCardsEditor extends Morph { for (let eventName of ['input']) { this.$id.addEventListener(eventName, evt => this.modify$id(evt, eventName), false); this.$name.addEventListener(eventName, evt => this.modify$name(evt), false); + this.$identity.addEventListener(eventName, evt => this.modify$identity(evt), false); this.$type.addEventListener(eventName, evt => this.modify$type(evt), false); this.$element.addEventListener(eventName, evt => this.modify$element(evt), false); this.$cost.addEventListener(eventName, evt => this.modify$cost(evt), false); @@ -29,25 +30,47 @@ export default class UBGCardsEditor extends Morph { } this.$text.addEventListener('keydown', evt => this.keydown$text(evt), false); this.$tagsInput.addEventListener('keydown', evt => this.keydown$tagInput(evt), false); - this.get('#rating').addEventListener('change', evt => { + const rating = this.get('#rating'); + rating.addEventListener('change', evt => { if (evt.target.name === 'rating') { this.modify$rating(evt) } }); + rating.addEventListener('keydown', evt => { + const key = evt.key; + lively.notify(key) + if (key >= '1' && key <= '9') { + const index = parseInt(key, 10) - 1; + lively.notify('key', index) + const radioButtonName = 'rating'; + const radioButtons = rating.querySelectorAll(`input[type="radio"][name="${radioButtonName}"]`); + + if (index < radioButtons.length) { // Check if the index is within the range of your radio buttons + const button = radioButtons[index] + button.checked = true; + button.focus() + const changeEvent = new Event('change', { + 'bubbles': true, // Allows the event to bubble up through the DOM + 'cancelable': false // Indicates the event cannot be canceled + }); + radioButtons[index].dispatchEvent(changeEvent); + } + } + }); } get ubg() { return lively.allParents(this, undefined, true).find(ele => ele.tagName === 'UBG-CARDS' || ele.tagName === 'JSPDF-EXAMPLE'); } - onKeyDown(evt) { - return; - if (evt.ctrlKey && evt.key == "s") { - evt.stopPropagation(); - evt.preventDefault(); - lively.warn(evt.key, 'key from editor') - } - } + // onKeyDown(evt) { + // return; + // if (evt.ctrlKey && evt.key == "s") { + // evt.stopPropagation(); + // evt.preventDefault(); + // lively.warn(evt.key, 'key from editor') + // } + // } selectedEntries() { return Array.from(this.querySelectorAll("lively-bibtex-entry.selected")); @@ -76,6 +99,9 @@ export default class UBGCardsEditor extends Morph { get $name() { return this.get('#name'); } + get $identity() { + return this.get('#identity'); + } get $type() { return this.get('#type'); } @@ -148,6 +174,21 @@ export default class UBGCardsEditor extends Morph { this.$name.value = name === undefined ? '' : name; } + modify$identity(evt) { + const identity = this.$identity.value; + if (identity === '') { + this.card.setIdentity(); + } else { + this.card.setIdentity(identity); + } + + this.propagateChange() + } + display$identity() { + const identity = this.card.getIdentity(); + this.$identity.value = identity === undefined ? '' : identity; + } + modify$type(evt) { const type = this.$type.value; if (type === '') { @@ -458,6 +499,7 @@ export default class UBGCardsEditor extends Morph { this.display$id(); this.display$name(); + this.display$identity(); this.display$type(); this.display$element(); this.display$cost(); @@ -484,7 +526,6 @@ export default class UBGCardsEditor extends Morph { delete this._delayedUpdateCardPreview; this.renderToHTML() - await this.renderToPDF() } renderToHTML() { @@ -492,23 +533,15 @@ export default class UBGCardsEditor extends Morph { const ubg = this.ubg; const cardPreview = document.createElement('ubg-card') - cardPreview.setAttribute('id', 'preview2') - this.get('#preview2').replaceWith(cardPreview) + cardPreview.setAttribute('id', 'preview') + this.get('#preview').replaceWith(cardPreview) cardPreview.setCard(card) - cardPreview.src = ubg.src + cardPreview.setCards(ubg.cards) + cardPreview.setSrc(ubg.src) cardPreview.render() } - async renderToPDF() { - const card = this.card; - const ubg = this.ubg; - - const pdf = await ubg.buildSingleCard(card); - this.get('#preview').replaceWith(
) - await ubg.showPDFData(pdf.output('dataurlstring'), this.get('#preview'), this.get('#previewViewer'), 'ubg-cards-editor'); - } - selectedEntry() { return this.table.asJSO()[this.table.currentRowIndex - 1]; } diff --git a/src/components/widgets/ubg-cards-entry.html b/src/components/widgets/ubg-cards-entry.html index e8c10f793..c79b59575 100644 --- a/src/components/widgets/ubg-cards-entry.html +++ b/src/components/widgets/ubg-cards-entry.html @@ -11,6 +11,8 @@ position: relative; background: rgba(150, 150, 150, .2); /* border-bottom: 1px solid green; */ + user-select: none; + user-drag: element; } :host(.is-bad) { diff --git a/src/components/widgets/ubg-cards-entry.js b/src/components/widgets/ubg-cards-entry.js index 6b74a9626..0431b62cc 100644 --- a/src/components/widgets/ubg-cards-entry.js +++ b/src/components/widgets/ubg-cards-entry.js @@ -1,10 +1,14 @@ import Morph from 'src/components/widgets/lively-morph.js'; +import { uuid, getTempKeyFor, fileName, hintForLabel, listAsDragImage, textualRepresentation } from 'utils'; export default class UBGCardEntry extends Morph { async initialize() { this.windowTitle = "UBGCardEntry"; this.registerButtons(); this.addEventListener('click', evt => this.clicked(evt)); + this.draggable = 'true' + this.addEventListener('dragstart', evt => (lively.notify(112343), this.onDragStart(evt))); + this.updateView(); } @@ -13,6 +17,10 @@ export default class UBGCardEntry extends Morph { } clicked(evt) { + this.selectMe() + } + + selectMe() { this.ubg.selectEntry(this); } @@ -156,6 +164,27 @@ export default class UBGCardEntry extends Morph { }[element && element.toLowerCase()]; } + /*MD ## Drag & Drop Cards MD*/ + // #TODO, #Stub: do proper multi selection of cards first + get multiSelection() { + return { + getSelectedItems: () => [this.ubg.card] + } + } + + onDragStart(evt) { + const selectedItems = this.multiSelection.getSelectedItems(); + if(!selectedItems.includes(this.card)) { + this.selectMe() + } + + const ubg = this.ubg; + if(ubg) { + ubg.addDragInfoTo(evt); + } + } + + /*MD ## Lively-specific API MD*/ livelyMigrate(other) { this.value = other.value; this.updateView(); diff --git a/src/components/widgets/ubg-cards.html b/src/components/widgets/ubg-cards.html index ecd71c9eb..c89330ecc 100644 --- a/src/components/widgets/ubg-cards.html +++ b/src/components/widgets/ubg-cards.html @@ -12,6 +12,15 @@ bottom: 0; } + :host(.over.accept-drop) { + background-color: lightgreen; +/* border: 2px green solid; */ + } + :host(.over.reject-drop) { + background-color: lightpink; +/* border: 2px red solid; */ + } + #content { background-color: lightgray; border: 1px solid gray; @@ -36,32 +45,11 @@ display: inline; } - #container { - display: none; - - position: absolute; - background: gray; - border: 1px solid black; - z-index: 10000; - width: 80%; - height: 40%; - top: 100px; - right: 10px; - overflow: auto; - } - :host(.show-preview) #container { - display: inherit; - } - - #viewerContainer { - position: absolute; - } - #layout { display: grid; grid-template-columns: auto 200px; - grid-template-rows: auto auto auto; - grid-template-areas: + grid-template-rows: auto 1fr 3.5in; + grid-template-areas: "header header" "main stats" "footer footer" @@ -147,14 +135,12 @@ - - + - - +
@@ -165,10 +151,4 @@
-
-
-
-
- -
diff --git a/src/components/widgets/ubg-cards.js b/src/components/widgets/ubg-cards.js index 3e5e4191c..31e9cf3b1 100644 --- a/src/components/widgets/ubg-cards.js +++ b/src/components/widgets/ubg-cards.js @@ -6,6 +6,8 @@ import "src/external/pdf.js"; import { shake } from 'utils'; import { Point } from 'src/client/graphics.js' +import { uuid, without, getTempKeyFor, getObjectFor, flatMap, listAsDragImage } from 'utils'; + import paper from 'src/client/paperjs-wrapper.js' import 'https://lively-kernel.org/lively4/ubg-assets/load-assets.js'; @@ -19,143 +21,9 @@ await preloaWebComponents(['ubg-card']) const POKER_CARD_SIZE_INCHES = lively.pt(2.5, 3.5); const POKER_CARD_SIZE_MM = POKER_CARD_SIZE_INCHES.scaleBy(25.4); -class FontCache { - - constructor() { - this.fonts = {}; - } - - /*MD ## Font Loading & Parsing MD*/ - async getFile(path) { - if (this.fonts[path]) { - lively.notify('cache hit') - } else { - lively.notify('cache miss') - this.fonts[path] = this.getBase64Font(path) - } - - return this.fonts[path]; - } - - async getBase64Font(url) { - async function blobToBase64String(blob) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - // Extract the Base64 encoded string - const base64String = reader.result.split(',')[1]; - resolve(base64String); - }; - reader.readAsDataURL(blob); - }) - } - - const response = await fetch(url) - if (!response.ok) { - throw new Error('Network response was not ok.'); - } - const blob = await response.blob(); - return blobToBase64String(blob) - } - - /*MD ## Font Asset Locations MD*/ - get FONT_ASSETS_FOLDER() { - return 'https://lively-kernel.org/lively4/ubg-assets/fonts/'; - } - - async getFontAwesomeFont(fileName) { - const FONT_AWESOME_FONT_FOLDER = this.FONT_ASSETS_FOLDER + 'fontawesome-v6.1.1/webfonts/'; - return this.getBase64Font(FONT_AWESOME_FONT_FOLDER + fileName) - } - - async BASE64_FONT_AWESOME_THIN() { - return this.getFontAwesomeFont('fa-thin-100.ttf') - } - async BASE64_FONT_AWESOME_LIGHT() { - return this.getFontAwesomeFont('fa-light-300.ttf') - } - async BASE64_FONT_AWESOME_REGULAR() { - return this.getFontAwesomeFont('fa-regular-400.ttf') - } - async BASE64_FONT_AWESOME_SOLID() { - return this.getFontAwesomeFont('fa-solid-900.ttf') - } - async BASE64_FONT_AWESOME_BRANDS() { - return this.getFontAwesomeFont('fa-brands-400.ttf') - } - async BASE64_FONT_AWESOME_DUOTONE() { - return this.getFontAwesomeFont('fa-duotone-900.ttf') - } - - async getRuneterraFont(fileName) { - const RUNETERRA_FONT_FOLDER = this.FONT_ASSETS_FOLDER + 'runeterra/fonts/'; - return this.getBase64Font(RUNETERRA_FONT_FOLDER + fileName) - } - - async BASE64_BeaufortforLOLJaBold() { - return this.getRuneterraFont('BeaufortforLOLJa-Bold.ttf') - } - async BASE64_BeaufortforLOLJaRegular() { - return this.getRuneterraFont('BeaufortforLOLJa-Regular.ttf') - } - async BASE64_Univers_55() { - return this.getRuneterraFont('univers_55.ttf') - } - async BASE64_Univers45LightItalic() { - return this.getRuneterraFont('univers-45-light-italic.ttf') - } - -} - -if (globalThis.__ubg_font_cache__) { - globalThis.__ubg_font_cache__.migrateTo(FontCache); -} else { - globalThis.__ubg_font_cache__ = new FontCache(); -} - -const BASE64_FONT_AWESOME_THIN = await globalThis.__ubg_font_cache__.BASE64_FONT_AWESOME_THIN() -const BASE64_FONT_AWESOME_LIGHT = await globalThis.__ubg_font_cache__.BASE64_FONT_AWESOME_LIGHT() -const BASE64_FONT_AWESOME_REGULAR = await globalThis.__ubg_font_cache__.BASE64_FONT_AWESOME_REGULAR() -const BASE64_FONT_AWESOME_SOLID = await globalThis.__ubg_font_cache__.BASE64_FONT_AWESOME_SOLID() -const BASE64_FONT_AWESOME_BRANDS = await globalThis.__ubg_font_cache__.BASE64_FONT_AWESOME_BRANDS() -const BASE64_FONT_AWESOME_DUOTONE = await globalThis.__ubg_font_cache__.BASE64_FONT_AWESOME_DUOTONE() - -const BASE64_BeaufortforLOLJaBold = await globalThis.__ubg_font_cache__.BASE64_BeaufortforLOLJaBold() -const BASE64_BeaufortforLOLJaRegular = await globalThis.__ubg_font_cache__.BASE64_BeaufortforLOLJaRegular() -const BASE64_Univers_55 = await globalThis.__ubg_font_cache__.BASE64_Univers_55() -const BASE64_Univers45LightItalic = await globalThis.__ubg_font_cache__.BASE64_Univers45LightItalic() - -const FONT_NAME_FA_THIN_100 = 'fa-thin-100' -const FONT_NAME_FA_LIGHT_300 = 'fa-light-300' -const FONT_NAME_FA_REGULAR_400 = 'fa-regular-400' -const FONT_NAME_FA_SOLID_900 = 'fa-solid-900' -const FONT_NAME_FA_BRANDS_400 = 'fa-brands-400' -const FONT_NAME_FA_DUOTONE_900 = 'fa-duotone-900' - -const FONT_NAME_BEAUFORT_FOR_LOL_BOLD = 'BeaufortforLOLJa-Bold' -const FONT_NAME_BEAUFORT_FOR_LOL_REGULAR = 'BeaufortforLOLJa-Regular' -const FONT_NAME_UNIVERS_55 = 'univers_55' -const FONT_NAME_UNIVERS_45_LIGHT_ITALIC = 'Univers 45 Light Italic' - -// Card group name (ELITE, SPIDER, YETI, etc.) -- Univers 59 // #BROKEN?? #TODO -const FONT_NAME_CARD_TYPE = FONT_NAME_UNIVERS_55 - -// Card name, card cost, card stats -- Beaufort for LOL Bold -const FONT_NAME_CARD_NAME = FONT_NAME_BEAUFORT_FOR_LOL_BOLD -const FONT_NAME_CARD_COST = FONT_NAME_BEAUFORT_FOR_LOL_BOLD -const FONT_NAME_CARD_VP = FONT_NAME_BEAUFORT_FOR_LOL_BOLD - -// Card description -- Univers 55 -const FONT_NAME_CARD_TEXT = FONT_NAME_UNIVERS_55 - const RUNETERRA_FONT_ID = 'runeterra-fonts' lively.loadCSSThroughDOM(RUNETERRA_FONT_ID, 'https://lively-kernel.org/lively4/ubg-assets/fonts/runeterra/css/runeterra.css') -const CSS_CLASS_BEAUFORT_FOR_LOL_BOLD = 'beaufort-for-lol-bold' -const CSS_CLASS_BEAUFORT_FOR_LOL_REGULAR = 'beaufort-for-lol-regular' -const CSS_CLASS_UNIVERS_55 = 'univers-55' -const CSS_CLASS_UNIVERS_45_LIGHT_ITALIC = 'univers-45-light-italic' - function identity(value) { return value; } @@ -200,32 +68,6 @@ function withGraphicsState(cb) { } } -async function getImageFromURL(url) { - const response = await fetch(url); - const blob = await response.blob(); - - const dataURL = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.addEventListener("load", () => { - resolve(reader.result); - }, false); - reader.readAsDataURL(blob); - }); - - const img = await new Promise((resolve, reject) => { - const img = new Image(); - img.onload = () => { - // img.src, - // img.height, - // img.width - resolve(img); - }; - img.src = dataURL; // could also just use the image file url here - }); - - return img; -} - const fire = ; const water = ; const earth = ; @@ -757,726 +599,6 @@ const SORT_BY = { NAME: 'name' }; -const VP_FILL = 'violet'; -const VP_STROKE = '#9400d3'; // darkviolet -const VP_FILL_ZERO = '#ddd'; -const VP_STROKE_ZERO = 'gray'; -const AFFECT_ALL_COLOR = 'rgba(255, 0, 0, 0.2)'; - -import 'src/external/dom-to-image.js' -const affectAllBackground = await (async function getAffectAllBackground() { - const div =
; - document.body.append(div) - try { - const dataUrl = await globalThis.domtoimage.toPng(div) - return `url(${dataUrl})`; - } finally { - div.remove(); - } -})() - -class RuleTextRenderer { - - static parseEffectsAndLists(printedRules) { - function prepRule(rule) { - return rule - return `${rule}` - } - - const lines = printedRules.split('\n'); - if (lines.length === 0) { - return printedRules; - } - - const result = [`
-${prepRule(lines.shift())}
`]; - - lines.forEach(line => { - const bulletMatch = line.match(/^\s*-\s*(.+)/); - if (bulletMatch) { - const content = bulletMatch[1]; - result.push(`
• ${prepRule(content)}
`); - } else { - result.push(`
${prepRule(line)}
`); - } - }); - - return result.join('\n'); - } - - static chip(text) { - return `${text}` - } - - static manaCost(element) { - const { others } = forElement(element); - - return SVG.inlineSVG(`${SVG.elementSymbol(element, lively.pt(5, 5), 5)} -${SVG.elementSymbol(others[0], lively.pt(12.5, 1.5), 1.5)} -${SVG.elementSymbol(others[1], lively.pt(13, 5), 1.5)} -${SVG.elementSymbol(others[2], lively.pt(12.5, 8.5), 1.5)}`, lively.rect(0, 0, 15, 10)); - } - - /*MD ## --- MD*/ - // #important - static async renderRuleText(cardEditor, cardDesc, doc, ruleBox, { - insetTextBy = 2, - beforeRenderRules = () => {} - } = { }) { - let printedRules = cardDesc.getText() || ''; - - // old big cast icon with small tap - // printedRules = printedRules.replace(/(^|\n)t3x(fire|water|earth|wind|gray)([^\n]*)/gi, function replacer(match, p1, pElement, pText, offset, string, groups) { - // return `
tap 3x${pElement}${pText}
`; - // }); - - // separate rules - printedRules = printedRules.replace(/affectAll(.*)\/affectAll/gmi, function replacer(match, innerText, offset, string, groups) { - return `
${innerText}
`; - }); - printedRules = this.parseEffectsAndLists(printedRules); - - printedRules = this.renderReminderText(printedRules, cardEditor, cardDesc) - - printedRules = printedRules.replace(/\b(?:\d|-|\+)*x(?:\d|-|\+|vp)*\b/gmi, function replacer(match, innerText, offset, string, groups) { - // find the bigger pattern, then just replace all x instead of reconstructing its surrounding characters - return match.replace('x', 'hedron') - }); - - printedRules = printedRules.replace(/blitz/gmi, ''); - printedRules = printedRules.replace(/passive/gmi, ''); - printedRules = printedRules.replace(/start of turn,?/gmi, ''); - printedRules = printedRules.replace(/ignition/gmi, ''); - printedRules = printedRules.replace(/\btrain\b/gmi, ''); - - // - printedRules = printedRules.replace(/\bcardname(?::(\d+))?/gmi, (match, cardId, offset, string, groups) => { - // lor blue card name #519ff1 - // #ffe967 - // #f8d66a - // #de9b75 - function highlightName(name) { - return `${name}` - } - if (!cardId) { - return highlightName(cardEditor.getNameFromCard(cardDesc)) - } - const card = cardEditor.cards.find(card => card.getId() + '' === cardId) - if (card) { - return highlightName(cardEditor.getNameFromCard(card)) - } else { - return `unknown id: ${cardId}` - } - }); - - - printedRules = printedRules.replace(/actionFree/gmi, () => this.chip('free')); - printedRules = printedRules.replace(/actionOnce/gmi, () => this.chip('once')); - printedRules = printedRules.replace(/actionMulti/gmi, () => this.chip('multi')); - printedRules = printedRules.replace(/actionMain:?/gmi, () => { - return '' - }); - - printedRules = this.renderCastIcon(printedRules) - - printedRules = printedRules.replace(/manaCost(fire|water|earth|wind|gray)/gmi, (match, pElement, offset, string, groups) => { - return this.manaCost(pElement); - }); - - printedRules = this.renderElementIcon(printedRules) - printedRules = this.renderVPIcon(printedRules) - printedRules = this.renderCardIcon(printedRules) - printedRules = this.renderCoinIcon(printedRules) - printedRules = this.renderBracketIcon(printedRules) - - printedRules = this.renderKeywords(printedRules) - printedRules = this.renderHedronIcon(printedRules) - printedRules = this.renderTapIcon(printedRules) - - printedRules = `${printedRules}` - - return this.renderToDoc(ruleBox, insetTextBy, printedRules, beforeRenderRules, doc) - } - - static renderReminderText(printedRules, cardEditor, cardDesc) { - function italic(text) { - return `${text}` - } - - return printedRules.replace(/\bremind(?:er)?(\w+(?:\-(\w|\(|\))*)*)\b/gmi, (match, myMatch, offset, string, groups) => { - const keywords = { - actionquest: () => { - return 'You may play this when you perform the action.' - }, - - affinity: (...args) => { - let subject = 'This costs' - if (args.includes('all')) { - args = args.filter(arg => arg !== 'all') - // keyword granted - subject = 'They cost' - } - - if (args.includes('power')) { - return subject + ' (x) less.' - } - - if (args.includes('vpchips')) { - return subject + ' (1) less per collected vp.' - } - - if (args.includes('coins')) { - return subject + ' (1) less per () you have.' - } - - if (args.includes('cards')) { - return subject + ' (1) less for each of those cards.' - } - - if (args.includes('mana')) { - const elements = args.filter(arg => arg !== 'mana') - let elementString - if (elements.length === 1) { - elementString = elements.first; - } else { - elementString = `${elements.slice(0, -1).join(', ')} or ${elements.last}`; - } - return subject + ` (1) less for each mana on ${elementString}.` - } - - throw new Error('unspecified type of Affinity') - }, - - blueprint: (cost) => { - 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.` - }, - - bound: (...args) => { - return 'Only exec bound abilities if the element is called.' - }, - - brittle: (...args) => { - if (args.includes('all')) { - // keyword granted - return 'Trash brittle cards after casting them.' - } - - return 'Trash this after casting it.' - }, - - convokecast: (...args) => { - if (args.includes('all')) { - // keyword granted - return 'Increase their x by 1 for each other card sharing an element with them.' - } - - return 'Increase this card\'s x by 1 for each other card sharing an element with it.' - }, - - countingquest: () => { - return 'If you fulfill its condition (track with []), as a free action you may trash this to create an Achievement Token.' - }, - - cycle: (cost) => { - if (cost) { - return `To cycle (${cost}), pay (${cost}) and trash the card to play a card of equal or lower cost.` - } - return `To cycle, trash the card to play a card of equal or lower cost.` - }, - - cycling: (cost, who) => { - let whoToPrint = 'this' - if (who === 'acard') { - whoToPrint = 'a card' - } else if (who === 'one') { - whoToPrint = 'the card' - } else if (who === 'all') { - whoToPrint = 'a card' - } - - if (cost) { - return `Passive As a free action, you may pay (${cost}) and trash ${whoToPrint} to play a card of equal or lower cost.` - } - return `Passive As a free action, you may trash ${whoToPrint} to play a card of equal or lower cost.` - }, - - dash: (cost, who) => { - let thatCard = 'this' - let it = 'this' - - if (who === 'one') { - thatCard = 'that card' - it = 'it' - } - - return `Pay (${cost}) to play ${thatCard}, but trash ${it} at end of turn.` - }, - - - delirium: () => { - // alternative: if you have cards of four different elements in trash. - return `Only activate delirium abilities if you have fire, water, earth and wind cards in trash.` - }, - - - discover: (howMany) => { - return `To discover ${howMany}, reveal top ${howMany} cards of any piles. Add 1 to your hand, trash the rest.` - }, - - emerge: (...args) => { - if (args.includes('all')) { - // keyword granted - return 'When you buy a card, you may trash a card for a discount equal to its cost.' - } - - if (args.includes('one')) { - // keyword granted - return 'When you buy the card, you may trash a card for a discount equal to its cost.' - } - - return 'When you buy this, you may trash a card for a discount equal to its cost.' - }, - - evoke: (cost, who) => { - if (who === 'all') { - return `As a free action, pay the cost and trash a card from hand to exec its blitz effects.` - } - if (who === 'one') { - return `As a free action, pay the cost and trash that card from hand to exec its blitz effects.` - } - return `As a free action, pay (${cost}) and trash this from hand to exec its blitz effects.` - }, - - flashback: (who) => { - let subject = 'this'; - if (who === 'all') { - subject = 'a card'; - } - if (who === 'one') { - subject = 'the card'; - } - return `Passive As a free action, you may trash ${subject} to exec its blitz effects.` - }, - - instant: () => { - return 'You may buy this as a free action.' - }, - - invoke: () => { - return 'You may trash this from hand or field to exec the effect.' - }, - - manaburst: () => { - return 'Only activate manaburst abilities if x is 4+.' - }, - - meld: () => { - return 'The melded card has all abilities and combined stats ((), vp, element, type) of its parts.' - }, - - postpone: (cost, delay) => { - 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.` - }, - - potion: (...args) => { - return `Trash this from hand or field to exec the effect.` - }, - - quest: () => { - return 'As a free action, you may play this if you fulfill its condition.' - }, - - quickcast: (...args) => { - if (args.includes('all')) { - // keyword granted - return 'Blitz You may cast it.' - } - if (args.includes('one')) { - // keyword granted - return 'Blitz You may cast it.' - } - - return 'Blitz You may cast this.' - }, - - resonance: (...args) => { - if (args.includes('all')) { - // keyword granted - return 'While a card\'s element is called, you may cast it along your main spell.' - } - if (args.includes('one')) { - // keyword granted - return 'While that card\'s element is called, you may cast it along your main spell.' - } - if (args.includes('this')) { - // variable element known - return 'While this card\'s element is called, you may cast this along your main spell.' - } - const elements = cardEditor.getElementsFromCard(cardDesc, false) - let elementString; - if (elements.length === 0 || (elements.length === 1 && elements.first === 'gray')) { - elementString = 'this card\'s element'; - } else if (elements.length === 1) { - elementString = elements.first; - } else { - elementString = `${elements.slice(0, -1).join(', ')} or ${elements.last}`; - } - - // Goal: While wind is called, you may cast this as a free action. - // While wind is called, you may cast this along another spell. - return `While ${elementString} is called, you may cast this along your main spell.` - }, - - saga: (...args) => { - return 'Blitz and Start of Turn Put [1] here. Then, exec the corresponding chapter\'s effect.' - }, - - seek: (...args) => { - return 'Reveal cards from any pile until you reveal the appropriate card(s), return the others to the game box.' - }, - - stuncounter: (...args) => { - return 'Casting a card with a stun counter removes the counter instead of the effect.' - }, - - tiny: () => { - return 'Tiny cards do not count for triggering the game end.' - }, - - trade: (...args) => { - return 'To trade, trash the card from hand to draw a card.' - }, - - trading: (who) => { - let whoText = 'this' - if (who === 'one') { - whoText = 'the card' - } else if (who === 'all') { - whoText = 'a card' - } - - return `Passive Once per turn as a free action, you may trash ${whoText} from hand to draw a card.` - }, - - upgrade: (diff, who) => { - let whoText = 'this' - if (who === 'one') { - whoText = 'the card' - } - - return `To upgrade, trash ${whoText} to play a card costing up to (${diff}) more.` - }, - }; - - const modifiers = myMatch.split('-') - const keyword = modifiers.shift() - const reminderText = keywords[keyword.toLowerCase()]; - if (!reminderText) { - lively.error(keyword, 'unknown reminder text') - return `unknown reminder text '${keyword}''`; - } - - return italic(`(${reminderText(...modifiers)})`); - }); - } - - static renderKeywords(printedRules) { - const C_DARKGRAY = '#555'; - const C_LIGHTGRAY = '#999'; - - function highlightKeyword(pattern, color=C_DARKGRAY, icon) { - printedRules = printedRules.replace(pattern, (match, pElement, offset, string, groups) => { - const text = match; - return `${icon || ''}${text}` - }); - } - - const C_DARKBEIGE = '#550'; - const C_BROWN = '#a50'; - - const C_RED_LIGHT = '#d44'; - const C_RED = '#f00'; - const C_DARKRED = '#a11'; - - const C_ORANGE = '#f50'; - - const C_GREEN_LIGHT = '#292'; - const C_GREEN = '#090'; - const C_GREEN_DARK = '#170'; - - const C_TEAL_LIGHT = '#085'; - const C_TEAL_DARK = '#164'; - - const C_TEAL_BLUE = '#05a'; - - const C_BLUE = '#00f'; - const C_BLUE_DARK = '#00c'; - - const C_BLUE_VIOLET = '#219'; - const C_VIOLET_BLUE = '#30a'; - - const C_VIOLET = '#708'; - - - highlightKeyword(/affinity\b/gmi, C_DARKBEIGE); - highlightKeyword(/\bbound\b(\sto)?/gmi, C_VIOLET_BLUE); - highlightKeyword(/brittle\b/gmi, C_RED); - highlightKeyword(/cycl(ed?|ing)\b/gmi, C_DARKGRAY); - highlightKeyword(/dash(ed|ing)?\b/gmi, C_BROWN); - highlightKeyword(/delirium:?\b/gmi, C_DARKGRAY); - highlightKeyword(/discover\b/gmi, C_DARKGRAY, ' '); - highlightKeyword(/manaburst\b:?/gmi, C_VIOLET, ' '); - highlightKeyword(/\b(un)?meld(ed)?\b/gmi, C_BLUE_VIOLET); - highlightKeyword(/potion\b/gmi, C_BLUE_VIOLET, ' '); - highlightKeyword(/quickcast\b/gmi, C_DARKGRAY); - highlightKeyword(/resonance\b/gmi, C_GREEN); - highlightKeyword(/seek\b/gmi, C_GREEN, ' '); - //'#3FDAA5' some turquise - 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%"', '')); - highlightKeyword(/upgraded?\b/gmi, C_ORANGE, SVG.inlineSVG(upgradeSVG.innerHTML, lively.rect(0, 0, 36, 36), 'x="10%" y="10%" width="80%" height="80%"', '')); - - return printedRules - } - - static renderElementIcon(printedRules) { - function inlineElement(element) { - return SVG.inlineSVG(SVG.elementSymbol(element, lively.pt(5, 5), 5)); - } - - return printedRules.replace(/\b(fire|water|earth|wind|gray)\b/gmi, (match, pElement, offset, string, groups) => inlineElement(pElement)); - } - - static renderHedronIcon(printedRules) { - function inlineHedron() { - return SVG.inlineSVG(hedronSVG.innerHTML, lively.rect(0, 0, 23, 23), 'x="10%" y="10%" width="80%" height="80%"', '') - } - - return printedRules.replace(/hedron/gmi, (match, pElement, offset, string, groups) => inlineHedron()); - } - - static renderTapIcon(printedRules) { - function inlineTapIcon() { - return SVG.inlineSVG(tapSVG.innerHTML, TAP_VIEWBOX, 'x="10%" y="10%" width="80%" height="80%"', '') - } - - return printedRules.replace(/\btap\b/gmi, (match, pElement, offset, string, groups) => inlineTapIcon()); - } - - static __textOnIcon__(text, rect, center) { - let textToPrint - if (text.includes('hedron') || text.includes('x')) { - const parts = [] - let isFirst = true; - for (let part of text.split(/x|hedron/i)) { - if (isFirst) { - isFirst = false - } else { - parts.push(`hedron`) - } - if (part) { // part is not an empty string - parts.push(part) - } - } - // split available space - const lengthPerPart = []; - for (let part of parts) { - const lengthOfPart = part === 'hedron' ? 1 : part.length - lengthPerPart.push(lengthOfPart) - } - const totalLength = lengthPerPart.sum() - const percentageSpacePerPart = lengthPerPart.map(len => len / totalLength) - let iteratingLength = 0 - textToPrint = parts.map((part, i) => { - let startingLength = iteratingLength - const endingLength = iteratingLength = startingLength + percentageSpacePerPart[i] - const middle = (startingLength + endingLength) / 2; - if (part === 'hedron') { - const scaleFactor = totalLength > 1 ? .7 : 1 - return `${part}` - } else { - return `${part}` - } - }).join('') - } else { - // simple form: just some text - textToPrint = `${text}`; - } - return textToPrint - } - - static renderVPIcon(printedRules) { - const printVP = vp => { - const rect = lively.rect(0, 0, 10, 10) - const center = rect.center(); - - let textToPrint = this.__textOnIcon__(vp, rect, center); - - Math.sqrt(.5) - return ` -${SVG.inlineSVG(` - -${textToPrint} -`)} -`; - } - - 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) { - return printVP(vp); - }); - } - - static renderCardIcon(printedRules) { - var that = this; - function inlineCardCost(cost) { - const rect = CARD_COST_ONE_VIEWBOX - const center = rect.center(); - - let textToPrint = that.__textOnIcon__(cost, rect, center); - return SVG.inlineSVG(`${cardCostOneSVG.innerHTML} -${textToPrint}`, CARD_COST_ONE_VIEWBOX, 'x="10%" y="10%" width="80%" height="80%"', '') - } - - return printedRules.replace(/\(\(((?:[*0-9xyz+-]|hedron)*)\)\)/gmi, (match, pElement, offset, string, groups) => inlineCardCost(pElement)); - } - - static renderCoinIcon(printedRules) { - const coin = text => { - const rect = lively.rect(0, 0, 10, 10) - const center = rect.center(); - - let textToPrint = this.__textOnIcon__(text, rect, center); - - return SVG.inlineSVG(`${SVG.circle(center, 5, `fill="goldenrod"`)} -${SVG.circleRing(center, 4.75, 5, `fill="darkviolet"`)} -${textToPrint}`); - } - - return printedRules.replace(/\(((?:[*0-9xyz+-]|hedron)*)\)/gmi, function replacer(match, p1, offset, string, groups) { - return coin(p1); - }); - } - - static renderBracketIcon(printedRules) { - const bracket = text => { - const rect = lively.rect(0, 0, 10, 10) - const center = rect.center(); - - let textToPrint = this.__textOnIcon__(text, rect, center); - - return SVG.inlineSVG(` - - -${textToPrint}`, undefined, undefined, 'transform:scale(1);'); - } - - return printedRules.replace(/\[((?:[*0-9xyz+-]|hedron)*)\]/gmi, function replacer(match, p1, offset, string, groups) { - return bracket(p1); - }); - } - - static renderCastIcon(printedRules) { - return printedRules.replace(/t?3x(fire|water|earth|wind|gray)\:?/gi, (match, pElement, offset, string, groups) => { - return `${castIcon} Cast:`; - }); - } - - static async renderToDoc(ruleBox, insetTextBy, printedRules, beforeRenderRules, doc) { - const textShadow = `text-shadow: - -1px -1px 0 #fff, - 1px -1px 0 #fff, - -1px 1px 0 #fff, - 1px 1px 0 #fff; - -1px 0 0 #fff, - 1px 0 0 #fff, - 0 1px 0 #fff, - 0 -1px 0 #fff;` - - const ruleTextBox = ruleBox.insetBy(insetTextBy); - // doc.rect(ruleBox.x, ruleBox.y, ruleBox.width, ruleBox.height, 'FD') - - const elementHTML =
; - document.body.append(elementHTML); - elementHTML.innerHTML = printedRules; - - const canvas = await html2canvas(elementHTML, { - backgroundColor: null, - ignoreElements: element => { - try { - if (!element) { - return true; - } - - return !(element === document.head || element.id === RUNETERRA_FONT_ID || element === document.body || element === elementHTML || elementHTML.contains(element)); - } catch (e) {} - } - }); - // elementHTML.remove(); - - const EXISTING_CANVAS_ID = 'exist-canvas'; - const EXISTING_ELEMENT_ID = 'exist-element'; - const existCanvas = document.getElementById(EXISTING_CANVAS_ID); - existCanvas && existCanvas.remove(); - document.body.appendChild(canvas); - canvas.id = EXISTING_CANVAS_ID; - - const existElement = document.getElementById(EXISTING_ELEMENT_ID); - existElement && existElement.remove(); - document.body.appendChild(elementHTML); - elementHTML.style.overflow = 'visible'; - elementHTML.id = EXISTING_ELEMENT_ID; - - const imgData = canvas.toDataURL('image/png'); - const imgRect = lively.rect(0, 0, canvas.width, canvas.height); - const scaledRect = imgRect.fitToBounds(ruleTextBox, true); - scaledRect.y = ruleTextBox.y + ruleTextBox.height - scaledRect.height; - - beforeRenderRules(scaledRect) - - doc.addImage(imgData, "PNG", ...scaledRect::xYWidthHeight()); - - return scaledRect - } -} - -function justify(pdfGen, text, xStart, yStart, textWidth) { - text = text.replace(/(?:\r\n|\r|\n)/g, ' '); - text = text.replace(/ +(?= )/g, ''); - const lineHeight = pdfGen.getTextDimensions('a').h * 1.15; - const words = text.split(' '); - let lineNumber = 0; - let wordsInfo = []; - let lineLength = 0; - for (const word of words) { - const wordLength = pdfGen.getTextWidth(word + ' '); - if (wordLength + lineLength > textWidth) { - writeLine(pdfGen, wordsInfo, lineLength, lineNumber++, xStart, yStart, lineHeight, textWidth); - wordsInfo = []; - lineLength = 0; - } - wordsInfo.push({ text, wordLength }); - lineLength += wordLength; - } - if (wordsInfo.length > 0) { - writeLastLine(wordsInfo, pdfGen, xStart, yStart, lineNumber, lineHeight); - } -} -function writeLastLine(wordsInfo, pdfGen, xStart, yStart, lineNumber, lineHeight) { - const line = wordsInfo.map(x => x.text).join(' '); - pdfGen.text(line, xStart, yStart + lineNumber * lineHeight); -} - -function writeLine(pdfGen, wordsInfo, lineLength, lineNumber, xStart, yStart, lineHeight, textWidth) { - - const wordSpacing = (textWidth - lineLength) / (wordsInfo.length - 1); - let x = xStart; - const y = yStart + lineNumber * lineHeight; - for (const wordInfo of wordsInfo) { - pdfGen.text(wordInfo.text, x, y); - x += wordInfo.wordLength + wordSpacing; - } -} - const OUTSIDE_BORDER_ROUNDING = lively.pt(3, 3) export default class Cards extends Morph { @@ -1513,6 +635,11 @@ export default class Cards extends Morph { this.rangeStart.addEventListener(eventName, evt => this.rangeChanged(evt), false); this.rangeEnd.addEventListener(eventName, evt => this.rangeChanged(evt), false); } + + this.addEventListener('dragenter', evt => this.dragenter(evt), false); + this.addEventListener('dragover', evt => this.dragover(evt), false); + this.addEventListener('dragleave', evt => this.dragleave(evt), false); + this.addEventListener('drop', evt => this.drop(evt), false); } /*MD ## Filter MD*/ @@ -1875,18 +1002,6 @@ export default class Cards extends Morph { return source; } - get viewerContainer() { - return this.get('#viewerContainer'); - } - - openInNewTab(doc) { - window.open(doc.output('bloburl'), '_blank'); - } - - async quicksavePDF(doc) { - doc.save('cards.pdf'); - } - getAllTags() { if (!this._allTags) { const tagCount = new Map(); @@ -1905,197 +1020,11 @@ export default class Cards extends Morph { } /*MD ## Build MD*/ - async ensureJSPDFLoaded() { - await lively.loadJavaScriptThroughDOM('jspdf', lively4url + '/src/external/jspdf/jspdf.umd.js'); - await lively.loadJavaScriptThroughDOM('svg2pdf', lively4url + '/src/external/jspdf/svg2pdf.umd.js'); - await lively.loadJavaScriptThroughDOM('html2canvas', lively4url + '/src/external/jspdf/html2canvas.js'); - } - - async createPDF(config) { - await this.ensureJSPDFLoaded(); - return new jspdf.jsPDF(config); - } - - async buildSingleCard(card) { - const doc = await this.createPDF({ - orientation: 'p', - unit: 'mm', - format: POKER_CARD_SIZE_MM.toPair - // putOnlyUsedFonts:true, - // floatPrecision: 16 // or "smart", default is 16 - () }); - - return this.buildCards(doc, [card], true); - } - - async buildFullPDF(cards) { - const doc = await this.createPDF({ - orientation: 'p', - unit: 'mm' - // format: POKER_CARD_SIZE_MM.addXY(5, 5).toPair(), - // putOnlyUsedFonts:true, - // floatPrecision: 16 // or "smart", default is 16 - }); - - return this.buildCards(doc, cards, false); // .slice(0,12) - } - async fetchAssetsInfo() { return (await this.assetsFolder.fetchStats()).contents; } - /*MD #### Fonts MD*/ - addFont(doc, vfsName, fontName, fontDataBase64) { - // "data:font/ttf;base64," + - const fontBase64 = fontDataBase64; - doc.addFileToVFS(vfsName, fontBase64); - doc.addFont(vfsName, fontName, 'normal'); - } - - async ensureFont(doc, fontName) { - if (!doc.__ubg_fonts__) { - doc.__ubg_fonts__ = {} - } - - if (doc.__ubg_fonts__[fontName]) { - lively.notify(fontName, 'existing font') - return - } - - doc.__ubg_fonts__[fontName] = true - - // convert fonts to jspdf-compatible format at https://peckconsulting.s3.amazonaws.com/fontconverter/fontconverter.html - const allFontData = { - [FONT_NAME_FA_THIN_100]: { - vfsName: 'fa-thin-100.ttf', - fontDataBase64: BASE64_FONT_AWESOME_THIN - }, - [FONT_NAME_FA_LIGHT_300]: { - vfsName: 'fa-light-300.ttf', - fontDataBase64: BASE64_FONT_AWESOME_LIGHT - }, - [FONT_NAME_FA_REGULAR_400]: { - vfsName: 'fa-regular-400.ttf', - fontDataBase64: BASE64_FONT_AWESOME_REGULAR - }, - [FONT_NAME_FA_SOLID_900]: { - vfsName: 'fa-solid-900.ttf', - fontDataBase64: BASE64_FONT_AWESOME_SOLID - }, - [FONT_NAME_FA_BRANDS_400]: { - vfsName: 'fa-brands-400.ttf', - fontDataBase64: BASE64_FONT_AWESOME_BRANDS - }, - [FONT_NAME_FA_DUOTONE_900]: { - vfsName: 'fa-duotone-900.ttf', - fontDataBase64: BASE64_FONT_AWESOME_DUOTONE - }, - - [FONT_NAME_BEAUFORT_FOR_LOL_BOLD]: { - vfsName: 'BeaufortforLOLJa-Bold-normal.ttf', - fontDataBase64: BASE64_BeaufortforLOLJaBold - }, - [FONT_NAME_BEAUFORT_FOR_LOL_REGULAR]: { - vfsName: 'BeaufortforLOLJa-Regular-normal.ttf', - fontDataBase64: BASE64_BeaufortforLOLJaRegular - }, - [FONT_NAME_UNIVERS_55]: { - vfsName: 'univers_55-normal.ttf', - fontDataBase64: BASE64_Univers_55 - }, - [FONT_NAME_UNIVERS_45_LIGHT_ITALIC]: { - vfsName: 'univers-45-light-italic.ttf', - fontDataBase64: BASE64_Univers45LightItalic - }, - } - - const fontData = allFontData[fontName] - if (!fontData) { - throw new Error('Unknown font: ' + fontName) - } - const { vfsName, fontDataBase64 } = fontData - this.addFont(doc, vfsName, fontName, fontDataBase64); - } - - async setAndEnsureFont(doc, fontName, fontStyle) { - // return; - await this.ensureFont(doc, fontName) - doc.setFont(fontName, fontStyle) - } - /*MD ### BUILD MD*/ - async buildCards(doc, cardsToPrint, skipCardBack) { - globalThis.doc = doc - const GAP = lively.pt(.2, .2); - - const rowsPerPage = Math.max(((doc.internal.pageSize.getHeight() + GAP.y) / (POKER_CARD_SIZE_MM.y + GAP.y)).floor(), 1); - const cardsPerRow = Math.max(((doc.internal.pageSize.getWidth() + GAP.x) / (POKER_CARD_SIZE_MM.x + GAP.x)).floor(), 1); - const cardsPerPage = rowsPerPage * cardsPerRow; - - const margin = lively.pt(doc.internal.pageSize.getWidth(), doc.internal.pageSize.getHeight()).subPt(lively.pt(cardsPerRow, rowsPerPage).scaleByPt(POKER_CARD_SIZE_MM).addPt(lively.pt(cardsPerRow - 1, rowsPerPage - 1).scaleByPt(GAP))); - - function progressLabel(numCard) { - return `process cards ${numCard}/${cardsToPrint.length}`; - } - const progress = await lively.showProgress(progressLabel(0)); - - if (!skipCardBack) { - doc.addPage("p", "mm", "a4"); - } - - try { - const assetsInfo = await this.fetchAssetsInfo(); - - let i = 0; - let currentPage = 0; - while (i < cardsToPrint.length) { - progress.value = (i + 1) / cardsToPrint.length; - progress.textContent = progressLabel(i); - - const indexOnPage = i % cardsPerPage; - const intendedPage = (i - indexOnPage) / cardsPerPage; - // lively.notify(`${i} ${indexOnPage} ${intendedPage}`); - if (currentPage < intendedPage) { - doc.addPage("p", "mm", "a4"); - doc.addPage("p", "mm", "a4"); - currentPage++; - lively.notify(currentPage) - } - const frontPage = 2 * currentPage + 1 - doc.setPage(frontPage) - - const rowIndex = (indexOnPage / rowsPerPage).floor(); - const columnIndex = indexOnPage % cardsPerRow; - const offset = lively.pt(columnIndex * (POKER_CARD_SIZE_MM.x + GAP.x), rowIndex * (POKER_CARD_SIZE_MM.y + GAP.y)).addPt(margin.scaleBy(1 / 2)); - const outsideBorder = offset.extent(POKER_CARD_SIZE_MM); - - // a.æÆ() - const cardToPrint = cardsToPrint[i]; - await this.renderCard(doc, cardToPrint, outsideBorder, assetsInfo); - - if (!skipCardBack) { - const backPage = frontPage + 1 - doc.setPage(backPage) - - const rowIndex = (indexOnPage / rowsPerPage).floor(); - const columnIndex = cardsPerRow - 1 - indexOnPage % cardsPerRow; - const offset = lively.pt(columnIndex * (POKER_CARD_SIZE_MM.x + GAP.x), rowIndex * (POKER_CARD_SIZE_MM.y + GAP.y)).addPt(margin.scaleBy(1 / 2)); - const outsideBorder = offset.extent(POKER_CARD_SIZE_MM); - - // a.æÆ() - const cardToPrint = cardsToPrint[i]; - await this.renderCardBack(doc, cardToPrint, outsideBorder, assetsInfo); - } - - i++; - } - } finally { - progress.remove(); - } - - return doc; - } - get editor() { return this.get('#editor'); } @@ -2151,15 +1080,10 @@ export default class Cards extends Morph { } /*MD ## Rendering MD*/ - async renderCard(doc, cardDesc, outsideBorder, assetsInfo) { - debugger - if (this.useOldMagicStyle()) { - return await this.renderMagicStyle(doc, cardDesc, outsideBorder, assetsInfo) - } else { - return await this.renderFullBleedStyle(doc, cardDesc, outsideBorder, assetsInfo) - } + getSkipCardbacks() { + return this.get('#skip-cardbacks') } - + get cardFrameStyle() { return this.get('#magic-style') } @@ -2167,869 +1091,7 @@ export default class Cards extends Morph { useOldMagicStyle() { return this.cardFrameStyle.checked } - - async getBackgroundImage(doc, cardDesc, bounds, assetsInfo) { - const filePath = this.filePathForBackgroundImage(cardDesc, assetsInfo); - return await this.loadBackgroundImageForFile(filePath, bounds) - } - - filePathForBackgroundImage(cardDesc, assetsInfo) { - const id = cardDesc.id; - const typeString = cardDesc.getType() && cardDesc.getType().toLowerCase && cardDesc.getType().toLowerCase() - const assetFileName = id + '.jpg'; - - if (id && assetsInfo.find(entry => entry.type === 'file' && entry.name === assetFileName)) { - return this.assetsFolder + assetFileName; - } - - const defaultFiles = { - gadget: 'default-gadget.jpg', - character: 'default-character.jpg', - spell: 'default-spell.jpg' - }; - return this.assetsFolder + (defaultFiles[typeString] || 'default.jpg'); - } - - async loadBackgroundImageForFile(filePath, bounds) { - const img = await globalThis.__ubg_file_cache__.getFile(filePath, getImageFromURL); - const imgRect = lively.rect(0, 0, img.width, img.height); - const scaledRect = imgRect.fitToBounds(bounds, true); - - return { img, scaledRect } - } - - async renderMagicStyle(doc, cardDesc, outsideBorder, assetsInfo) { - const currentVersion = cardDesc.versions.last; - - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - - // black border - doc::withGraphicsState(() => { - doc.setFillColor(0.0); - doc.roundedRect(...outsideBorder::xYWidthHeight(), 3, 3, 'F'); - }); - - // innerBorder - const innerBorder = outsideBorder.insetBy(3); - // doc.setFillColor(120, 120, 120); - // doc.roundedRect(...innerBorder::xYWidthHeight(), 3, 3, 'FD'); - - // id - doc::withGraphicsState(() => { - doc.setFontSize(7); - doc.setTextColor(255, 255, 255); - doc.text(`${cardDesc.id || '???'}/1`, innerBorder.right(), (innerBorder.bottom() + outsideBorder.bottom()) / 2, { align: 'right', baseline: 'middle' }); - }); - - // card image - const { img, scaledRect } = await this.getBackgroundImage(doc, cardDesc, innerBorder, assetsInfo); - - doc::withGraphicsState(() => { - doc.rect(...innerBorder::xYWidthHeight(), null); // set clipping area - doc.internal.write('W n'); - doc.addImage(img, "JPEG", ...scaledRect::xYWidthHeight()); - }); - - // title bar - const TITLE_BAR_HEIGHT = 7; - const titleBar = innerBorder.insetBy(1); - titleBar.height = TITLE_BAR_HEIGHT; - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: 0.5 })); - doc.setFillColor(BOX_FILL_COLOR); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.roundedRect(...titleBar::xYWidthHeight(), 1, 1, 'DF'); - }); - - // card name - const cardName = this.getNameFromCard(cardDesc); - doc::withGraphicsState(() => { - doc.setFontSize(.6 * TITLE_BAR_HEIGHT::mmToPoint()); - doc.setTextColor('#000000'); - doc.text(cardName, ...titleBar.leftCenter().addX(2).toPair(), { align: 'left', baseline: 'middle' }); - }); - // doc.text(['hello world', 'this is a card'], ...titleBar.leftCenter().addX(2).toPair(), { align: 'left', baseline: 'middle' }); - - // cost - const cost = cardDesc.getCost(); - const costs = Array.isArray(cost) ? cost : [cost]; - let top = titleBar.bottom() + 1; - let right = titleBar.right(); - const COIN_RADIUS = 4; - costs.forEach((cost, i) => { - const coinCenter = lively.pt(right - COIN_RADIUS, top + COIN_RADIUS + COIN_RADIUS * 2 * 0.9 * i); - - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: 0.9 })); - doc.setFillColor('#b8942d'); - doc.setDrawColor('#b8942d'); - doc.ellipse(...coinCenter.toPair(), COIN_RADIUS, COIN_RADIUS, 'DF'); - }); - - if (cost !== undefined) { - doc::withGraphicsState(() => { - doc.setFontSize(12); - doc.setTextColor('#000000'); - doc.text('' + cost, ...coinCenter.toPair(), { align: 'center', baseline: 'middle' }); - }); - } - }); - - // rule box - const ruleBox = innerBorder.insetBy(1); - const height = innerBorder.height * .4; - ruleBox.y = ruleBox.bottom() - height; - ruleBox.height = height; - doc::withGraphicsState(() => { - doc.get - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.roundedRect(...ruleBox::xYWidthHeight(), 1, 1, 'DF'); - }); - - // rule text - const ubgTest = document.querySelector('#ubg-test'); - if (ubgTest && false) { - lively.notify(244); - await new Promise((resolve, reject) => { - doc.html(ubgTest, { - callback: resolve, - x: ruleBox.x, - y: ruleBox.y - }); - }); - } else {} - // const ruleTextBox = ruleBox.insetBy(2); - // doc.setFontSize(9); - // doc.setTextColor('#000000'); - // doc.text(currentVersion.text, ...ruleTextBox.topLeft().toPair(), { align: 'left', baseline: 'top', maxWidth: ruleTextBox.width }); - - - // type & elements - doc::withGraphicsState(() => { - doc.setFontSize(7); - doc.setTextColor(255, 255, 255); - doc.text(`${currentVersion.type || ''} - ${currentVersion.elements || currentVersion.element || ''}`, ruleBox.left(), ruleBox.top() - .5, { align: 'justify', baseline: 'bottom' }); - }); - - await this.renderRuleText(doc, cardDesc, ruleBox, { - insetTextBy: 2 - }); - - // tags - const tagsAnchor = ruleBox.topRight().subY(1); - await this.renderTags(doc, cardDesc, tagsAnchor) - } - - async renderFullBleedStyle(doc, cardDesc, outsideBorder, assetsInfo) { - const type = cardDesc.getType(); - const typeString = type && type.toLowerCase && type.toLowerCase() || ''; - - if (typeString === 'spell') { - await this.renderSpell(doc, cardDesc, outsideBorder, assetsInfo) - } else if (typeString === 'gadget') { - await this.renderGadget(doc, cardDesc, outsideBorder, assetsInfo) - } else if (typeString === 'character') { - await this.renderCharacter(doc, cardDesc, outsideBorder, assetsInfo) - } else { - await this.renderMagicStyle(doc, cardDesc, outsideBorder, assetsInfo) - } - - this.renderIsBad(doc, cardDesc, outsideBorder) - this.renderVersionIndicator(doc, cardDesc, outsideBorder) - } - - /*MD ### Rendering Card Types MD*/ - // #important - async renderSpell(doc, cardDesc, outsideBorder, assetsInfo) { - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - - // background card image - const { img, scaledRect } = await this.getBackgroundImage(doc, cardDesc, outsideBorder, assetsInfo); - this.withinCardBorder(doc, outsideBorder, () => { - doc.addImage(img, "JPEG", ...scaledRect::xYWidthHeight()); - }); - - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - doc.rect(...outsideBorder::xYWidthHeight(), 'F'); - }); - }) - - // spell circle - { - const CIRCLE_BORDER = -3; - const RADIUS = (outsideBorder.width - CIRCLE_BORDER) / 2; - const middle = outsideBorder.center().withY(outsideBorder.top() + CIRCLE_BORDER + RADIUS) - - // console.log(doc.getLineWidth()) - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.circle(...middle.toPair(), RADIUS, null); - doc.internal.write('W n'); - - doc.addImage(img, "JPEG", ...scaledRect::xYWidthHeight()); - - doc.setDrawColor(BOX_STROKE_COLOR); - doc.setLineWidth(2) - doc.circle(...middle.toPair(), RADIUS, 'D'); - }) - }) - } - - // innerBorder - const innerBorder = outsideBorder.insetBy(3); - // doc.setFillColor(120, 120, 120); - // doc.roundedRect(...innerBorder::xYWidthHeight(), 3, 3, 'FD'); - - // title - const TITLE_BAR_HEIGHT = 7; - const COST_COIN_RADIUS = 4; - const COST_COIN_MARGIN = 2; - - const titleBorder = innerBorder.insetBy(1); - titleBorder.height = TITLE_BAR_HEIGHT; - - await this.renderTitleBarAndCost(doc, cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) - - // rule box - const ruleBox = outsideBorder.copy() - const height = outsideBorder.height * .3; - ruleBox.y = ruleBox.bottom() - height; - ruleBox.height = height; - // const ruleBox = innerBorder.insetBy(1); - // const height = innerBorder.height * .4; - // ruleBox.y = ruleBox.bottom() - height; - // ruleBox.height = height; - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - // doc.rect(...ruleBox::xYWidthHeight(), 'F'); - }) - }) - - doc::withGraphicsState(() => { - doc.setLineWidth(1); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.setLineDashPattern([2,1], 0); - // doc.line(ruleBox.left(), ruleBox.top(), ruleBox.right(), ruleBox.top()); - }); - - // rule text - const ruleTextBox = await this.renderRuleText(doc, cardDesc, ruleBox, { - insetTextBy: 2 - }); - - // tags - const tagsAnchor = ruleTextBox.topRight(); - await this.renderTags(doc, cardDesc, tagsAnchor) - - // id - this.renderId(doc, cardDesc, outsideBorder, innerBorder) - } - - // #important - async renderGadget(doc, cardDesc, outsideBorder, assetsInfo) { - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - - // background card image - const { img, scaledRect } = await this.getBackgroundImage(doc, cardDesc, outsideBorder, assetsInfo); - this.withinCardBorder(doc, outsideBorder, () => { - doc.addImage(img, "JPEG", ...scaledRect::xYWidthHeight()); - }); - - // innerBorder - const innerBorder = outsideBorder.insetBy(3); - // doc.setFillColor(120, 120, 120); - // doc.roundedRect(...innerBorder::xYWidthHeight(), 3, 3, 'FD'); - - // top box - const ruleBox2 = outsideBorder.copy() - ruleBox2.height = 13; - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - doc.rect(...ruleBox2::xYWidthHeight(), 'F'); - }) - }) - - doc::withGraphicsState(() => { - doc.setLineWidth(1); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.setLineDashPattern([2,0], 0); - doc.line(ruleBox2.left(), ruleBox2.bottom(), ruleBox2.right(), ruleBox2.bottom()); - }); - - // title - const TITLE_BAR_HEIGHT = 7; - const COST_COIN_RADIUS = 4; - const COST_COIN_MARGIN = 2; - - const titleBorder = innerBorder.insetBy(1); - titleBorder.height = TITLE_BAR_HEIGHT; - - await this.renderTitleBarAndCost(doc, cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) - - // rule box border calc - const ruleBox = outsideBorder.copy() - const height = outsideBorder.height * .4; - ruleBox.y = ruleBox.bottom() - height; - ruleBox.height = height; - - // rule text - const RULE_TEXT_INSET = 2; - let effectiveRuleBox - const ruleTextBox = await this.renderRuleText(doc, cardDesc, ruleBox, { - insetTextBy: RULE_TEXT_INSET, - beforeRenderRules: ruleTextBox => { - // rule box render - effectiveRuleBox = ruleTextBox.insetBy(-RULE_TEXT_INSET) - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - doc.rect(...effectiveRuleBox::xYWidthHeight(), 'F'); - }) - }) - - doc::withGraphicsState(() => { - doc.setLineWidth(1); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.line(effectiveRuleBox.left(), effectiveRuleBox.top(), effectiveRuleBox.right(), effectiveRuleBox.top()); - }); - } - }); - - // tags - const tagsAnchor = lively.pt(ruleTextBox.right(), effectiveRuleBox.top()).subY(1); - await this.renderTags(doc, cardDesc, tagsAnchor) - - // id - this.renderId(doc, cardDesc, outsideBorder, innerBorder) - } - - // #important - async renderCharacter(doc, cardDesc, outsideBorder, assetsInfo) { - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - - // background card image - const { img, scaledRect } = await this.getBackgroundImage(doc, cardDesc, outsideBorder, assetsInfo); - this.withinCardBorder(doc, outsideBorder, () => { - doc.addImage(img, "JPEG", ...scaledRect::xYWidthHeight()); - }); - - // Zohar design - const ZOHAR_DESIGN_BORDER_WIDTH = .5; - [[outsideBorder.topLeft(), lively.pt(1, 0)], [outsideBorder.topRight(), lively.pt(-1, 0)]].forEach(([startingPt, direction]) => { - const dirX = direction.x; - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: 0.5 })); - doc.setFillColor(BOX_FILL_COLOR); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.setLineWidth(ZOHAR_DESIGN_BORDER_WIDTH); - doc.lines([[dirX*8,0],[0,15],[-dirX*15,15],[dirX*15,15],[0,100], [-dirX*10,0]], ...startingPt.toPair(), [1,1], 'DF', true) - }); - }); - }) - - // innerBorder - const innerBorder = outsideBorder.insetBy(3); - // doc.setFillColor(120, 120, 120); - // doc.roundedRect(...innerBorder::xYWidthHeight(), 3, 3, 'FD'); - - // title - const TITLE_BAR_HEIGHT = 7; - const COST_COIN_RADIUS = 4; - const COST_COIN_MARGIN = 2; - - const titleBorder = innerBorder.insetBy(1); - titleBorder.height = TITLE_BAR_HEIGHT; - - await this.renderTitleBarAndCost(doc, cardDesc, titleBorder, COST_COIN_RADIUS, COST_COIN_MARGIN) - - // rule box border calc - const ruleBox = outsideBorder.copy() - const height = outsideBorder.height * .4; - ruleBox.y = ruleBox.bottom() - height; - ruleBox.height = height; - - // rule text - const RULE_TEXT_INSET = 2; - let effectiveRuleBox - const ruleTextBox = await this.renderRuleText(doc, cardDesc, ruleBox, { - insetTextBy: RULE_TEXT_INSET, - beforeRenderRules: ruleTextBox => { - // rule box render - effectiveRuleBox = ruleTextBox.insetBy(-RULE_TEXT_INSET) - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - doc.rect(...effectiveRuleBox::xYWidthHeight(), 'F'); - }) - }) - - doc::withGraphicsState(() => { - doc.setLineWidth(1); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.line(effectiveRuleBox.left(), effectiveRuleBox.top(), effectiveRuleBox.right(), effectiveRuleBox.top()); - }); - } - }); - - // tags - const tagsAnchor = lively.pt(ruleTextBox.right(), effectiveRuleBox.top()).subY(1); - await this.renderTags(doc, cardDesc, tagsAnchor) - - // id - this.renderId(doc, cardDesc, outsideBorder, innerBorder) - } - - /*MD ### Rendering Card Components MD*/ - withinCardBorder(doc, outsideBorder, cb) { - function clipOuterBorder() { - doc.roundedRect(...outsideBorder::xYWidthHeight(), OUTSIDE_BORDER_ROUNDING.x, OUTSIDE_BORDER_ROUNDING.y, null); // set clipping area - doc.internal.write('W n'); - } - - if (isAsync(cb)) { - return doc::withGraphicsState(async () => { - clipOuterBorder() - return await cb(); - }); - } else { - return doc::withGraphicsState(() => { - clipOuterBorder() - return cb(); - }); - } - } - - async renderTitleBarAndCost(doc, cardDesc, border, costCoinRadius, costCoinMargin) { - const TITLE_BAR_BORDER_WIDTH = 0.200025; - - const titleBar = border.copy() - const coinLeftCenter = titleBar.leftCenter() - const spacingForCoin = 2*costCoinRadius + costCoinMargin - titleBar.x += spacingForCoin - titleBar.width -= spacingForCoin - - // title space -// doc::withGraphicsState(() => { -// const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - -// doc.setGState(new doc.GState({ opacity: 0.5 })); -// doc.setFillColor('ffffff'); -// doc.rect(...border::xYWidthHeight(), 'F'); -// }); - - - // title bar - doc::withGraphicsState(() => { - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - doc.setGState(new doc.GState({ opacity: .5 })); - doc.setFillColor(BOX_FILL_COLOR); - doc.setDrawColor(BOX_STROKE_COLOR); - doc.setLineWidth(TITLE_BAR_BORDER_WIDTH); - doc.roundedRect(...titleBar::xYWidthHeight(), 1, 1, 'DF'); - }); - - // card name - await doc::withGraphicsState(async () => { - await this.setAndEnsureFont(doc, FONT_NAME_CARD_NAME, "normal") - doc.setFontSize(.6 * titleBar.height::mmToPoint()); - doc.setTextColor('#000000'); - doc.text(this.getNameFromCard(cardDesc), ...titleBar.leftCenter().addX(2).toPair(), { - align: 'left', - baseline: 'middle', - maxWidth: titleBar.width - }); - }); - - const coinCenter = coinLeftCenter.addX(costCoinRadius); - await this.renderInHandSymbols(doc, cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) - } - - async renderInHandSymbols(doc, cardDesc, border, costCoinRadius, costCoinMargin, coinCenter) { - let currentCenter = coinCenter; - - // cost - await this.renderCost(doc, cardDesc, currentCenter, costCoinRadius) - - if ((cardDesc.getType() || '').toLowerCase() !== 'character') { - // vp - currentCenter = currentCenter.addY(costCoinRadius * 2.75); - await this.renderBaseVP(doc, cardDesc, currentCenter, costCoinRadius) - - // element (list) - currentCenter = currentCenter.addY(costCoinRadius * 2.75); - const elementListDirection = 1; - currentCenter = await this.renderElementList(doc, cardDesc, currentCenter, costCoinRadius, elementListDirection) - } else { - currentCenter = currentCenter.addY(costCoinRadius * 1); - } - - // type - currentCenter = currentCenter.addY(costCoinRadius * .75) - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - await this.renderType(doc, cardDesc, currentCenter, BOX_FILL_COLOR, BOX_FILL_OPACITY) - } - - async renderElementList(doc, cardDesc, pos, radius, direction) { - const elements = this.getElementsFromCard(cardDesc, true); - for (let element of elements) { - await this.renderElementSymbol(doc, element, pos, radius) - pos = pos.addY(direction * radius * .75); - } - return pos.addY(direction * radius * .25); - } - - async renderCost(doc, cardDesc, pos, coinRadius) { - const costSize = coinRadius / 3; - - const costDesc = cardDesc.getCost(); - const cost = Array.isArray(costDesc) ? costDesc.first : costDesc; - - const coinCenter = pos; - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: 0.9 })); - doc.setFillColor('#b8942d'); - doc.setDrawColor(148, 0, 211); - doc.setLineWidth(0.2 * costSize) - doc.circle(...coinCenter.toPair(), coinRadius, 'DF'); - }); - - await this.renderIconText(doc, coinCenter, costSize, cost, FONT_NAME_CARD_COST) - } - - async renderBaseVP(doc, cardDesc, pos, coinRadius) { - const costSize = coinRadius / 3; - - const vp = cardDesc.getBaseVP() || 0; - - const iconCenter = pos; - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: 0.9 })) - // doc.setFillColor('#b8942d'); - doc.setDrawColor(vp === 0 ? VP_STROKE_ZERO : VP_STROKE) - doc.setLineWidth(0.2 * costSize) - // doc.circle(...coinCenter.toPair(), coinRadius, 'DF'); - doc.setFillColor(vp === 0 ? VP_FILL_ZERO : VP_FILL) - // doc.rect(coinCenter.x - coinRadius, coinCenter.y - coinRadius, 2 * coinRadius, 2 * coinRadius, 'DF'); - - // diamond shape - const diagonal = coinRadius * .9 * Math.sqrt(2) - const rightAbsolute = iconCenter.addX(diagonal).toPair() - const down = lively.pt(-diagonal, diagonal).toPair() - const left = lively.pt(-diagonal, -diagonal).toPair() - const up = lively.pt(diagonal, -diagonal).toPair() - const rightAgain = lively.pt(diagonal, diagonal).toPair() - doc.lines([down, left, up, rightAgain], ...rightAbsolute, [1,1], 'DF', true) - }); - - await this.renderIconText(doc, iconCenter, costSize, vp, FONT_NAME_CARD_VP) - } - - async renderIconText(doc, centerPos, size, text, font) { - if (text === undefined) { - return - } - - await doc::withGraphicsState(async () => { - await this.setAndEnsureFont(doc, font, "normal") - doc.setFontSize(12 * size); - doc.setTextColor('#000000'); - doc.text('' + text, ...centerPos.toPair(), { align: 'center', baseline: 'middle' }); - }); - } - - // #important - async renderRuleText(doc, cardDesc, ruleBox, options = { }) { - return RuleTextRenderer.renderRuleText(this, cardDesc, doc, ruleBox, options) - options?.beforeRenderRules?.(lively.rect(10,10,20,20)) - return lively.rect(10,10,20,20) - } - - // type - async renderType(doc, cardDesc, anchorPt, color, opacity) { - // const typeAndElementAnchor = anchorPt - await doc::withGraphicsState(async () => { - doc.setGState(new doc.GState({ opacity: opacity })); - doc.setFillColor(color); - - // function curate() { - // return this.toLower().upperFirst(); - // } - // function prepend(other) { - // return other + ' ' + this; - // } - // const element = cardDesc.getElement(); - let fullText = (cardDesc.getType() || '').toLower().upperFirst() - // if (Array.isArray(element)) { - // element.forEach(element => { - // fullText = fullText::prepend(element::curate()) - // }) - // } else if (element) { - // fullText = fullText::prepend(element::curate()) - // } - await this.setAndEnsureFont(doc, FONT_NAME_CARD_TYPE, "normal") - doc.setFontSize(7); - - const { w, h: textHeight } = doc.getTextDimensions(fullText); - - const typeElementTextBox = anchorPt.subX(w/2).extent(lively.pt(w, textHeight)) - const typeElementTextBoxExpansion = 1 - const typeElementBox = typeElementTextBox.expandBy(typeElementTextBoxExpansion) - const roundedCorner = textHeight/2 + typeElementTextBoxExpansion - doc.roundedRect(...typeElementBox::xYWidthHeight(), roundedCorner, roundedCorner, 'F'); - - doc.setTextColor('000'); - doc.text(fullText, typeElementTextBox.left(), typeElementTextBox.centerY(), { align: 'justify', baseline: 'middle' }); - }) - } - - renderTags(doc, cardDesc, tagsAnchor) { - const tags = cardDesc.getTags().sortBy(i => i, false).map(tag => '#' + tag); - doc::withGraphicsState(() => { - const FONT_SIZE = 7; - doc.setFontSize(FONT_SIZE); - // text dimensions only work well for single-line text - const { w, h } = doc.getTextDimensions(tags.first || ''); - doc.setTextColor('black'); - for (let text of tags) { - doc.text(text, ...tagsAnchor.toPair(), { align: 'right', baseline: 'bottom' }); - tagsAnchor = tagsAnchor.subY(h) - } - }); - } - - async renderElementSymbol(doc, element, pos, radius) { - const svgInnerPos = lively.pt(5, 5); - const svgInnerRadius = 5; - const yourSvgString = SVG.inlineSVG(SVG.elementSymbol(element, svgInnerPos, svgInnerRadius)) - - let container = document.getElementById('svg-container'); - if (!container) { - container =
; - document.body.append(container) - } - container.innerHTML = yourSvgString - const svgElement = container.firstElementChild - // force layout calculation - svgElement.getBoundingClientRect() - // const width = svgElement.width.baseVal.value - // const height = svgElement.height.baseVal.value - - await doc.svg(svgElement, { - x: pos.x - radius, - y: pos.y - radius, - width: radius * 2, - height: radius * 2 - }) - - // doc::withGraphicsState(() => { - // doc.setGState(new doc.GState({ opacity: .8 })); - // doc.setFillColor(stroke); - // doc.ellipse(...pos.subXY(radius, radius).toPair(), 1, 1, 'F') - // }) - } - - renderId(doc, cardDesc, outsideBorder, innerBorder, color = '000') { - doc::withGraphicsState(() => { - doc.setFontSize(7); - doc.setTextColor(color); - doc.text(`${cardDesc.id || '???'}/${cardDesc.getHighestVersion()}`, innerBorder.right() - 2, (innerBorder.bottom() + outsideBorder.bottom()) / 2, { align: 'right', baseline: 'middle' }); - }); - } - - renderIsBad(doc, cardDesc, outsideBorder) { - function slash(color, width=2, offset=lively.pt(0,0)) { - doc::withGraphicsState(() => { - doc.setDrawColor(color); - doc.setLineWidth(width) - doc.line(outsideBorder.right() + offset.x, outsideBorder.top() + offset.y, outsideBorder.left() + offset.x, outsideBorder.bottom() + offset.y); - }); - } - - 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)) - } - } - renderVersionIndicator(doc, cardDesc, outsideBorder) { - const VERSION_FILL = '#f7d359'; - const renderDiamond = (pos, radius) => { - this.withinCardBorder(doc, outsideBorder, () => { - const iconCenter = pos; - doc::withGraphicsState(() => { - doc.setGState(new doc.GState({ opacity: 0.9 })) - doc.setDrawColor(VP_STROKE) - doc.setLineWidth(0.2) - doc.setFillColor(VERSION_FILL) - - // diamond shape - const diagonal = radius * .9 * Math.sqrt(2) - const rightAbsolute = iconCenter.addX(diagonal).toPair() - const down = lively.pt(-diagonal, diagonal).toPair() - const left = lively.pt(-diagonal, -diagonal).toPair() - const up = lively.pt(diagonal, -diagonal).toPair() - const rightAgain = lively.pt(diagonal, diagonal).toPair() - doc.lines([down, left, up, rightAgain], ...rightAbsolute, [1,1], 'F', true) - }); - }); - } - renderDiamond(outsideBorder.bottomRight(), 3.5) - // renderDiamond(outsideBorder.topRight(), 4.5) - } - - async renderCardBack(doc, cardDesc, outsideBorder, assetsInfo) { - const [BOX_FILL_COLOR, BOX_STROKE_COLOR, BOX_FILL_OPACITY] = this.colorsForCard(cardDesc); - - // background card image - const backgroundImageFile = this.assetsFolder + 'default-spell.jpg' - const { img, scaledRect } = await this.loadBackgroundImageForFile(backgroundImageFile, outsideBorder); - this.withinCardBorder(doc, outsideBorder, () => { - doc.addImage(img, "JPEG", ...scaledRect::xYWidthHeight()); - }); - - // inner border - { - const CIRCLE_BORDER = -3; - const RADIUS = (outsideBorder.width - CIRCLE_BORDER) / 2; - const middle = outsideBorder.center().withY(outsideBorder.top() + CIRCLE_BORDER + RADIUS) - - // console.log(doc.getLineWidth()) - this.withinCardBorder(doc, outsideBorder, () => { - doc.saveGraphicsState(); - - doc.circle(...outsideBorder.center().toPair(), outsideBorder.height * .5 * .9, null) - - // globalThis.doc = doc - // doc.moveTo(10, 10) - // doc.lineTo(10, 100) - // doc.lineTo(50, 100) - // doc.lineTo(10, 10) - - doc.clipEvenOdd() - doc.saveGraphicsState(); - - doc.setGState(new doc.GState({ opacity: BOX_FILL_OPACITY })); - doc.setFillColor(BOX_FILL_COLOR); - doc.rect(...outsideBorder::xYWidthHeight(), 'F'); - - doc.restoreGraphicsState(); - doc.restoreGraphicsState(); - }) - } - - // element symbols - await this.withinCardBorder(doc, outsideBorder, async () => { - const innerBorder = outsideBorder.insetBy(3); - const RADIUS = 5 - await this.renderElementList(doc, cardDesc, innerBorder.topLeft().addXY(RADIUS, RADIUS), RADIUS, 1) - await this.renderElementList(doc, cardDesc, innerBorder.topRight().addXY(-RADIUS, RADIUS), RADIUS, 1) - await this.renderElementList(doc, cardDesc, innerBorder.bottomLeft().addXY(RADIUS, -RADIUS), RADIUS, -1) - await this.renderElementList(doc, cardDesc, innerBorder.bottomRight().addXY(-RADIUS, -RADIUS), RADIUS, -1) - }) - - // outerBorder - this.withinCardBorder(doc, outsideBorder, () => { - doc::withGraphicsState(() => { - doc.setDrawColor(BOX_STROKE_COLOR); - // only actually draw half of this, other half is masked - doc.setLineWidth(2) - doc.roundedRect(...outsideBorder::xYWidthHeight(), OUTSIDE_BORDER_ROUNDING.x, OUTSIDE_BORDER_ROUNDING.y, 'D'); - }) - }) - } - - /*MD ## Preview MD*/ - async loadPDFFromURLToBase64(url) { - // Loading document - // Load a blob, transform the blob into base64 - // Base64 is the format we need since it is editable and can be shown by PDFJS at the same time. - const response = await fetch(url); - const blob = await response.blob(); - // let fileReader = new FileReader(); - // fileReader.addEventListener('loadend', () => { - // // this.editedPdfText = atob(fileReader.result.replace("data:application/pdf;base64,", "")); - // }); - - // fileReader.readAsDataURL(blob); - return URL.createObjectURL(blob); - } - - async showPDFData(base64pdf, container, viewer, context = 'ubg-cards-preview') { - { - const old = this.pdfContext && this.pdfContext[context]; - if (old) { - old.pdfDocument.destroy(); - old.pdfViewer.cleanup(); - this.pdfContext[context] = undefined; - } - } - - const eventBus = new window.PDFJSViewer.EventBus(); - const pdfLinkService = new window.PDFJSViewer.PDFLinkService({ eventBus }); - const pdfViewer = new window.PDFJSViewer.PDFViewer({ - eventBus, - container, - viewer, - linkService: pdfLinkService, - renderer: "canvas", // svg canvas - textLayerMode: 1 - }); - pdfLinkService.setViewer(pdfViewer); - container.addEventListener('pagesinit', () => { - pdfViewer.currentScaleValue = 1; - }); - - const pdfDocument = await PDFJS.getDocument(base64pdf).promise; - pdfViewer.setDocument(pdfDocument); - pdfLinkService.setDocument(pdfDocument, null); - - (this.pdfContext = this.pdfContext || {})[context] = { - eventBus, - pdfLinkService, - pdfViewer, - pdfDocument - }; - - await pdfViewer.pagesPromise; - // #TODO can we advice the pdfView to only render the current page we need? - // if (this.getAttribute("mode") != "scroll") { - // this.currentPage = 1 - // this.showPage(this.getPage(this.currentPage)) - // } - } - - /*MD ## --- MD*/ - // toBibtex() { - // var bibtex = ""; - // for (var ea of this.querySelectorAll("lively-bibtex-entry")) { - // bibtex += ea.innerHTML; - // } - // return bibtex; - // } - /*MD ## Sorting MD*/ get sortBy() { return this.getAttribute('sortBy') || SORT_BY.ID; @@ -3278,23 +1340,10 @@ export default class Cards extends Morph { const cardsToPrint = this.filterCardsForPrinting(filteredEntries.map(entry => entry.card)) if (await this.checkForLargePrinting(cardsToPrint)) { - await this.printForExport(cardsToPrint, evt.shiftKey, false); + await this.printForExport(cardsToPrint); } } - async onPrintSelectedNew() { - if (!this.cards) { - return; - } - - const filteredEntries = this.allEntries.filter(entry => entry.isVisible()) - const cardsToPrint = this.filterCardsForPrinting(filteredEntries.map(entry => entry.card)) - - if (await this.checkForLargePrinting(cardsToPrint)) { - await this.printForExport(cardsToPrint, false, true); - } - } - async onPrintChanges(evt) { if (!this.cards) { return; @@ -3303,7 +1352,7 @@ export default class Cards extends Morph { const cardsToPrint = this.filterCardsForPrinting(this.cards.filter(card => !card.getIsPrinted())); if (await this.checkForLargePrinting(cardsToPrint)) { - await this.printForExport(cardsToPrint, evt.shiftKey, false); + await this.printForExport(cardsToPrint); } } @@ -3315,7 +1364,7 @@ export default class Cards extends Morph { return true; } - async printForExport(cards, quickSavePDF, useHTMLPrinting) { + async printForExport(cards) { if (cards.length === 0) { lively.warn('no cards to print for export'); return; @@ -3334,17 +1383,8 @@ export default class Cards extends Morph { this.markAsChanged() } - if (useHTMLPrinting) { - await globalThis.__ubg_html_to_pdf_exporter__.execute(cards, this) - return; - } - - const doc = await this.buildFullPDF(cards); - if (quickSavePDF) { - this.quicksavePDF(doc); - } else { - this.openInNewTab(doc); - } + const skipCardBack = this.getSkipCardbacks(); + await globalThis.__ubg_html_to_pdf_exporter__.execute(cards, this, skipCardBack) } async onSaveJson(evt) { @@ -3358,30 +1398,6 @@ export default class Cards extends Morph { this.clearMarkAsChanged(); } - async onSavePdf(evt) { - const pdfUrl = this.src.replace(/\.json$/, '.pdf'); - - if (!await lively.confirm(`Save full cards as ${pdfUrl}?`)) { - return; - } - - const cardsToSave = this.cards.slice(0, 12); - const doc = await this.buildFullPDF(cardsToSave); - const blob = doc.output('blob'); - await lively.files.saveFile(pdfUrl, blob); - } - - async onShowPreview(evt) { - const cardsToPreview = this.cards.slice(0, 12); - const doc = await this.buildFullPDF(cardsToPreview); - this.classList.add('show-preview'); - await this.showPDFData(doc.output('dataurlstring'), this.viewerContainer); - } - - onClosePreview(evt) { - this.classList.remove('show-preview'); - } - async onAddButton(evt) { await this.addNewCard(); } @@ -3452,6 +1468,112 @@ export default class Cards extends Morph { this.removeAttribute('text-changed'); } + /*MD ## Drag & Drop Cards MD*/ + addDragInfoTo(evt) { + const dt = evt.dataTransfer; + // #TODO: An improved fix would be to change what is returned by the widget selection + const selectedCards = this.getCardsToTransmit(); + if(selectedCards.length > 0) { + dt.setData("javascript/object", getTempKeyFor(selectedCards)); + } else { + lively.error('no cards to drag'); + } + + dt.setData("ubg", ''); + dt.setData(this.getDataTransferID(), ''); + + listAsDragImage(selectedCards, evt, -10, 2); + } + + getCardsToTransmit() { + return this.card ? [this.card] : [] + } + + getDataTransferID() { + return "ubg/source-" + lively.ensureID(this) + } + + dragenter(evt) {} + dragover(evt) { + evt.preventDefault(); + + this._resetDropOverEffects(); + this.classList.add('over'); + + const dt = evt.dataTransfer; + const types = dt.types; + + if (!types.includes('ubg')) { + this.classList.add('reject-drop'); + return; + } + + if (types.includes(this.getDataTransferID())) { + // cannot drop on myself + this.classList.add('reject-drop'); + return; + } + + const hasData = dt.types.includes("javascript/object"); + if(!hasData) { + this.classList.add('reject-drop'); + return; + } + + this.classList.add('accept-drop'); + dt.dropEffect = "copy"; + } + + dragleave(evt) { + this._resetDropOverEffects(); + } + + _resetDropOverEffects() { + this.classList.remove('over'); + this.classList.remove('reject-drop'); + this.classList.remove('accept-drop'); + } + + async drop(evt) { + this._resetDropOverEffects(); + + const dt = evt.dataTransfer; + const types = dt.types; + + if (!types.includes('ubg')) { + return; + } + + if (types.includes(this.getDataTransferID())) { + lively.warn('cannot drop onto myself') + return; + } + + const hasData = dt.types.includes("javascript/object"); + if(!hasData) { + this.classList.add('reject-drop'); + return; + } + + evt.stopPropagation(); + + const myWindow = lively.allParents(this).find(ele => ele && ele.localName === 'lively-window') + if(myWindow) { + lively.gotoWindow(myWindow, true); + } else { + lively.notify('no window') + } + this.editor.focusOnText() + + const data = getObjectFor(dt.getData("javascript/object")); + const copiedCards = deserialize(serialize(data), { Card }) + await this.addCards(copiedCards) + this.selectCard(copiedCards.last); + lively.success(`Copied ${copiedCards.length} card(s)`); + + this.markAsChanged() + } + /*MD ## lively API MD*/ livelyMigrate(other) { this.cards = other.cards;