import { log, MODULE_ID } from "../debug-mode.js"; import { artGalleryDefaultSettings } from "../settings.js"; export class HelperFunctions { static MODULE_ID = "journal-to-canvas-slideshow"; /** * pass in a string and capitalize each word in the string * @param {String} string - the string whose words we want to capitalize * @param {String} delimiter - a delimiter separating each word * @returns A string with each word capitalized and the same delimiters */ static capitalizeEachWord(string, delimiter = " ") { let sentenceArray; let capitalizedString; if (!delimiter) { // if the delimiter is an empty string, split it by capital letters, as if camelCase sentenceArray = string.split(/(?=[A-Z])/).map((s) => s.toLowerCase()); capitalizedString = sentenceArray .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) .join(" "); } else { sentenceArray = string.split(delimiter); capitalizedString = sentenceArray .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(delimiter); } return capitalizedString; } static async resetArtGallerySettings() { await HelperFunctions.setSettingValue("artGallerySettings", {}, "", false); await HelperFunctions.setSettingValue( "artGallerySettings", artGalleryDefaultSettings, "", true ); // await game.settings.set(MODULE_ID, "artGallerySettings", newSettings); } static async swapTools(layerName = "background", tool = "select") { if (game.version >= 10) { if ((layerName = "background")) layerName = "tiles"; } ui.controls.controls.find((c) => c.layer === layerName).activeTool = tool; let ourLayer = game.canvas.layers.find((l) => l.options.name === layerName); if (ourLayer) { if (ourLayer && !ourLayer.active) { ourLayer?.activate(); } else { ui.controls.render(true); } } else { console.error("Can't find that layer", ourLayer); } } /** * Scale a tile's size, or one dimension (length or width) of a tile * @param {Number} scale - the ratio to scale the tile by * @param {String} axis - the tile's width or the tile's height or both */ static async scaleControlledTiles(scale = 0.5, axis = " ") { const ourScene = game.scenes.viewed; const layerName = game.version >= 10 ? "tiles" : "background"; const sceneTiles = canvas[layerName].controlled.filter( (obj) => obj.document.documentName === "Tile" ); let updateObjects = []; sceneTiles.forEach((tile) => { let tileWidth = game.version >= 10 ? tile.width : tile.data.width; let tileHeight = game.version >= 10 ? tile.height : tile.data.height; let width = duplicate(tileWidth); let height = duplicate(tileHeight); height *= scale; width *= scale; updateObjects.push({ _id: tile.id, ...(axis === " " && { height: height, width: width }), ...(axis === "height" && { height: height }), ...(axis === "width" && { width: width }), }); }); ourScene.updateEmbeddedDocuments("Tile", updateObjects); } // Move tiles // originally By @cole$9640 //slightly edited by @Eva into a modular method static async moveControlledTiles(amount = 10, axis = "x") { const ourScene = game.scenes.viewed; let tiles; if (game.version >= 10) { tiles = canvas.tiles.controlled; } else { tiles = canvas.background.controlled.length === 0 ? canvas.foreground.controlled : canvas.background.controlled; } if (tiles.length) { const updates = tiles .filter((tile) => { if (game.version >= 10) return !tile.locked; else return !tile.data.locked; }) .map((tile) => ({ _id: tile.id, [axis]: tile[axis] + amount, })); ourScene.updateEmbeddedDocuments("Tile", updates); } else { ui.notifications.notify("Please select at least one tile."); } } static async setFlagValue(document, flagName, updateData, nestedKey = "") { await document.setFlag(MODULE_ID, flagName, updateData); } /** * Get the value of a document's flag * @param {Object} document - the document whose flags we want to set (Scene, Actor, Item, etc) * @param {String} flagName - the name of the flag * @param {String} nestedKey - a string of nested properties separated by dot notation that we want to set * @param {*} returnIfEmpty - a value to return if the flag is undefined * @returns */ static async getFlagValue(document, flagName, nestedKey = "", returnIfEmpty = []) { let flagData = await document.getFlag(MODULE_ID, flagName); if (!flagData) { flagData = returnIfEmpty; } return flagData; } /** * Sets a value, using the "flattenObject" and "expandObject" utilities to reach a nested property * @param {String} settingName - the name of the setting * @param {*} updateData - the value you want to set a property to * @param {String} nestedKey - a string of dot-separated values to refer to a nested property */ static async setSettingValue( settingName, updateData, nestedKey = "", isFormData = false ) { if (isFormData) { let currentSettingData = game.settings.get( HelperFunctions.MODULE_ID, settingName ); updateData = expandObject(updateData); //get expanded object version of formdata keys, which were strings in dot notation previously updateData = mergeObject(currentSettingData, updateData); // let updated = await game.settings.set(HelperFunctions.MODULE_ID, settingName, currentSettingData); // console.warn(updated); } if (nestedKey) { let settingData = game.settings.get(HelperFunctions.MODULE_ID, settingName); setProperty(settingData, nestedKey, updateData); await game.settings.set(HelperFunctions.MODULE_ID, settingName, settingData); } else { await game.settings.set(HelperFunctions.MODULE_ID, settingName, updateData); } } /* --------------------------------- Colors --------------------------------- */ /** * * Get the contrasting color for any hex color * (c) 2019 Chris Ferdinandi, MIT License, https://gomakethings.com * Derived from work by Brian Suda, https://24ways.org/2010/calculating-color-contrast/ * @param {String} A hexcolor value * @return {String} The contrasting color (black or white) **/ static getContrast(hexcolor) { // If a leading # is provided, remove it if (hexcolor.slice(0, 1) === "#") { hexcolor = hexcolor.slice(1); } // If a three-character hexcode, make six-character if (hexcolor.length === 3) { hexcolor = hexcolor .split("") .map(function (hex) { return hex + hex; }) .join(""); } // Convert to RGB value var r = parseInt(hexcolor.substr(0, 2), 16); var g = parseInt(hexcolor.substr(2, 2), 16); var b = parseInt(hexcolor.substr(4, 2), 16); // Get YIQ ratio var yiq = (r * 299 + g * 587 + b * 114) / 1000; // Check contrast //@author: slightly modified this return yiq; // >= 128 ? "black" : "white"; // return yiq >= 128 ? "black" : "white"; } /** * Will return 1 if color is darker than gray, -1 if color is lighter than gray * @param {String} hexColor - string hex color * @returns {Number} -1 or +1 */ static lighterOrDarker(hexColor) { const HF = HelperFunctions; let yiq = HF.getContrast(hexColor); //the 128 is like a value between 0 and 255, so gray //checking to see if the contrast value is greater than gray (black) or less than gray (white) return yiq >= 128 ? -1 : 1; } /** * * @param {String} backgroundColor - our background color * @param {String} accentColor - the color we're adjusting to find a tint with better contrast * @returns a color lightened to have the right contrast */ static getColorWithContrast(backgroundColor, accentColor) { const HF = HelperFunctions; backgroundColor = HF.hex8To6(backgroundColor); accentColor = HF.hex8To6(accentColor); const contrastValue = HF.getContrastBetween(backgroundColor, accentColor); const direction = contrastValue > 0 ? 1 : -1; // const text = contrastValue < 0 ? "We should darken color" : "we should lighten color"; let adjustedColor = HF.hex8To6(accentColor); for ( let adjustAmount = 0, times = 0; times < 15; adjustAmount += direction * 10, times += 1 ) { adjustedColor = HF.LightenDarkenColor(accentColor, adjustAmount); const hasEnoughContrast = HF.checkIfColorsContrastEnough( backgroundColor, adjustedColor ); if (hasEnoughContrast) { break; } } return adjustedColor; } static getContrastBetween(backgroundColor, accentColor) { const HF = HelperFunctions; let contrast1 = HF.getContrast(backgroundColor); let contrast2 = HF.getContrast(accentColor); //the 128 is like a value between 0 and 255, so gray. //if luminance? is grater than 128, it's between gray and white, so return a dark color //if luminance? is less than 128, it's between black and gray, so return a light color let contrastBetween = contrast2 - contrast1; return contrastBetween; } static checkIfColorsContrastEnough(hexColor1, hexColor2) { const HF = HelperFunctions; let contrast1 = HF.getContrast(hexColor1); let contrast2 = HF.getContrast(hexColor2); //the 128 is like a value between 0 and 255, so gray. //if luminance? is grater than 128, it's between gray and white, so return a dark color //if luminance? is less than 128, it's between black and gray, so return a light color let contrastBetween = Math.abs(contrast2 - contrast1); return contrastBetween >= 128 ? true : false; } /** * Programmatically lighten or darken a color * @author "Pimp Trizkit" on Stackoverflow * @link https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors * @returns a lightened or darkened color */ static LightenDarkenColor(col, amt) { var usePound = false; if (col[0] == "#") { col = col.slice(1); usePound = true; } var num = parseInt(col, 16); var r = (num >> 16) + amt; if (r > 255) r = 255; else if (r < 0) r = 0; var b = ((num >> 8) & 0x00ff) + amt; if (b > 255) b = 255; else if (b < 0) b = 0; var g = (num & 0x0000ff) + amt; if (g > 255) g = 255; else if (g < 0) g = 0; var string = "000000" + (g | (b << 8) | (r << 16)).toString(16); return (usePound ? "#" : "") + string.substring(string.length - 6); // return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); } /** * set the custom colors for the indicators and color scheme in the JTCS Apps * @param {String} settingPropertyString - the string name of the property in the settings */ static async getColorDataFromSettings(settingPropertyString) { const HF = HelperFunctions; let colorData = await HelperFunctions.getSettingValue( "artGallerySettings", settingPropertyString ); let { colors, propertyNames, colorVariations } = colorData; Object.keys(colors).forEach((colorKey) => { const value = HF.hex8To6(colors[colorKey]); const propertyName = propertyNames[colorKey]; let shouldMakeVariants = false; if (colorVariations) { shouldMakeVariants = colorVariations[colorKey]; } HF.setRootStyleProperty(propertyName, value, shouldMakeVariants); }); // add these extra bits on for now if (settingPropertyString === "colorSchemeData") { let { accentColor, backgroundColor } = colors; // accentColor = accentColor;//HF.getColorWithContrast(backgroundColor, accentColor); backgroundColor = HF.hex8To6(backgroundColor); const colorNeutral = HF.getContrast(backgroundColor) >= 128 ? "black" : "white"; const textColor = HF.getContrast(accentColor) >= 128 ? "black" : "white"; HF.setRootStyleProperty("--JTCS-text-color-on-bg", colorNeutral); //for text on the background color HF.setRootStyleProperty("--JTCS-text-color-on-fill", textColor); //for text on buttons and filled labels HF.setRootStyleProperty("--JTCS-accent-color", accentColor); //HF.getColorWithContrast(backgroundColor, accentColor)); } } /** * set the custom colors for the indicators and color scheme in the JTCS Apps */ static async setUIColors() { await this.getColorDataFromSettings("indicatorColorData"); await this.getColorDataFromSettings("colorSchemeData"); } /** * Set properties of custom colors on root element for use in our CSS * @param {String} propertyName - the name of the CSS custom property we want to set on the root element * @param {String} value - a value representing a hex code color * @param {*} makeVariations - whether we should generate light and dark variants on this color for better contrast */ static setRootStyleProperty(propertyName, value, makeVariations = false) { const html = document.documentElement; const HF = HelperFunctions; value = HF.hex8To6(value); html.style.setProperty(propertyName, value); if (makeVariations) { const direction = HF.lighterOrDarker(value); const shouldDarken = direction < 0 ? true : false; const text = direction < 0 ? "We should darken color" : "we should lighten color"; let startNumber = !shouldDarken ? 80 : 0; let step = !shouldDarken ? -10 : 10; for (var number = startNumber; Math.abs(number) < 90; number += step) { const variantPropName = `${propertyName}-${number .toString() .padStart(2, "0")}`; // const amount = number; const amount = shouldDarken ? number * -1 : number; const variantValue = HF.LightenDarkenColor(value, amount); html.style.setProperty(variantPropName, variantValue); } if (propertyName.includes("background-color")) { const htmlStyle = getComputedStyle(html); let inputBG = htmlStyle.getPropertyValue("--JTCS-background-color"); let elevationBG = htmlStyle.getPropertyValue("--JTCS-background-color"); let borderColor = htmlStyle.getPropertyValue( "--JTCS-background-color-70" ); let shadowColor = htmlStyle.getPropertyValue( "--JTCS-background-color-50" ); let dangerColor = htmlStyle.getPropertyValue("--color-danger-base"); let warningColor = htmlStyle.getPropertyValue("--color-warning-base"); let infoColor = htmlStyle.getPropertyValue("--color-info-base"); let successColor = htmlStyle.getPropertyValue("--color-success-base"); let tileItemColor = "transparent"; if (!shouldDarken) { inputBG = getComputedStyle(html).getPropertyValue( "--JTCS-background-color-20" ); borderColor = "transparent"; //getComputedStyle(html).getPropertyValue("--JTCS-background-color"); shadowColor = "transparent"; elevationBG = htmlStyle.getPropertyValue( "--JTCS-background-color-10" ); dangerColor = htmlStyle.getPropertyValue("--color-danger-light"); dangerColor = htmlStyle.getPropertyValue("--color-danger-light"); infoColor = htmlStyle.getPropertyValue("--color-info-light"); successColor = htmlStyle.getPropertyValue("--color-success-light"); tileItemColor = elevationBG; } html.style.setProperty("--JTCS-box-shadow-color", shadowColor); html.style.setProperty("--JTCS-input-background-color", inputBG); html.style.setProperty("--JTCS-border-color", borderColor); html.style.setProperty("--JTCS-elevation-BG-color", elevationBG); html.style.setProperty("--JTCS-danger-color", dangerColor); html.style.setProperty("--JTCS-warning-color", warningColor); html.style.setProperty("--JTCS-info-color", dangerColor); html.style.setProperty("--JTCS-success-color", warningColor); html.style.setProperty("--JTCS-tile-item-bg-color", tileItemColor); } } } /** * Checks if color is in hex8 format, and if so slices string to make it hex6 * @param {String} hexColor - a hex color code with "#" up front * @returns {String} */ static hex8To6(hexColor) { let hexColorMod = hexColor; if (hexColor.slice(1).length > 6) { hexColorMod = hexColor.slice(0, -2); } return hexColorMod; } static async getSettingValue(settingName, nestedKey = "") { let settingData = await game.settings.get(HelperFunctions.MODULE_ID, settingName); if (settingData !== undefined && settingData !== null) { if (nestedKey) { let nestedSettingData = getProperty(settingData, nestedKey); return nestedSettingData; } return settingData; } else { console.error("Cannot find setting with name " + settingName); } } static async checkSettingEquals(settingName, compareToValue) { if (game.settings.get(HelperFunctions.MODULE_ID, settingName) == compareToValue) { return true; } return false; } static async showWelcomeMessage() { let options = {}; let d = new Dialog({ title: "Welcome Message", content: `

Journal To Canvas Slideshow Has Updated

Journal to Canvas Slideshow is now known as "JTCS - Art Gallery"

The module has received a huge overhaul, updates and improvements to preexisting features, and the addition of brand new features.

  1. View a detailed guide and walkthrough of the new features here
  2. ReadMe and Feature List
  3. Release Notes

Note: This welcome message can be turned on and off in the module settings, but will be enabled after updates to inform you of important changes.

`, buttons: { disable: { label: "Disable Welcome Message", callback: async () => await HelperFunctions.disableWelcomeMessage(), }, continue: { label: "Continue without Disabling", }, }, }); d.render(true); } static async disableWelcomeMessage() { //disable the welcome message await HelperFunctions.setSettingValue("showWelcomeMessage", false); } static isImage(url) { return /\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(url); } /** * Validating text input * @param {String} inputValue - the input value * @param {string} validationType - the type of input, what are we looking for, an image, video, etc. * @param {function} onInvalid - callback function for if our input is invalid * @returns true or false depending on if our input is valid or not */ static validateInput(inputValue, validationType, onInvalid = "") { let valid = false; switch (validationType) { case "image": valid = HelperFunctions.isImage(inputValue); break; default: valid = inputValue !== undefined; break; } return valid; } static getElementPositionAndDimension(element) { return {}; } static isOutsideClick(event) { if ($(event.target).closest(".popover").length) { //click was on the popover return false; } //if our click is outside of our popover element return true; } /** * Create a dialog * @param {String} title - the title of the dialog * @param {String} templatePath - the path to the template being used for this dialog * @param {Object} data - the data object * @param {Object} data.buttons - the buttons at the bottom of the prompt */ static async createDialog(title, templatePath, data) { const options = { width: 600, // height: 250, id: "JTCS-custom-dialog", }; let renderedHTML = await renderTemplate(templatePath, data); let d = new Dialog( { title: title, content: renderedHTML, buttons: data.buttons, }, options ).render(true); } static async createEventActionObject( name, callback, shouldRenderAppOnAction = false ) { return { name: name, callback: callback, shouldRenderAppOnAction: shouldRenderAppOnAction, }; } static editorsActive(sheet) { let hasActiveEditors = Object.values(sheet.editors).some( (editor) => editor.active ); return hasActiveEditors; } /// }