import { ArtTileManager } from "./ArtTileManager.js"; import { HelperFunctions } from "./HelperFunctions.js"; import ImageVideoPopout from "./MultiMediaPopout.js"; /** * This class manages the images specifically, setting and clearing the tiles' images */ export class ImageDisplayManager { static async getTilesFromArtScene() { let artSceneID = await game.JTCS.utils.getSettingValue( "artGallerySettings", "dedicatedDisplayData.scene.value" ); let ourScene = game.scenes.get(artSceneID); let artTiles = await game.JTCS.tileUtils.getSceneSlideshowTiles("art", true, { currentSceneID: artSceneID, }); let artTileID = artTiles[0].id; let frameTiles = await game.JTCS.tileUtils.getSceneSlideshowTiles("frame", true, { currentSceneID: artSceneID, }); let frameTileID = artTiles[0].linkedBoundingTile || frameTiles[0].id; return { ourScene: ourScene, artTileID: artTileID, frameTileID: frameTileID, }; } static async updateTileObjectTexture( artTileID, frameTileID, url, method, sceneID = "" ) { let ourScene = game.scenes.get(sceneID); if (!ourScene) ourScene = game.scenes.viewed; let artTile = ourScene.tiles.get(artTileID); let frameTile = ourScene.tiles.get(frameTileID); //load the texture from the source if (!artTile || !url) { ui.notifications.error("Tile or image not found"); console.error(url, artTile, artTileID); return; } const tex = await loadTexture(url); if (!tex) { ui.notifications.error( `Error loading texture from '${url}'. Access to URL likely blocked by CORS policy.` ); return; } let imageUpdate; if (!frameTile) { imageUpdate = await ImageDisplayManager.scaleArtTileToScene( artTile, tex, url, sceneID ); } else { imageUpdate = await ImageDisplayManager.scaleArtTileToFrameTile( artTile, frameTile, tex, url, sceneID ); } const updated = await ourScene .updateEmbeddedDocuments("Tile", [imageUpdate]) .catch((error) => ui.notifications.error( `Default art tile in ${ourScene.name} couldn't be updated` ) ); if (updated && method === "artScene") { const { autoActivate, autoView } = await HelperFunctions.getSettingValue( "artGallerySettings", "dedicatedDisplayData.scene" ); if (autoActivate) { ourScene.activate(); } if (autoView) { ourScene.view(); } if ( game.user.isGM && //if we're GM ((!autoActivate && !autoView) || (ourScene.active && !ourScene.viewed)) //if the scene is neither set to activate or to view, notify that the image updated. // if the scene is active but not viewed, notify that the image updated ) { ui.notifications.info( `Default Tile in Art Scene '${ourScene.name}' successfully updated` ); } } } static async scaleArtTileToScene(displayTile, tex, url, sceneID = "") { let displayScene = game.scenes.get(sceneID); if (!displayScene) displayScene = game.scenes.viewed; let displaySceneWidth = game.version >= 10 ? displayScene.width : displayScene.data.width; let displaySceneHeight = game.version >= 10 ? displayScene.height : displayScene.data.height; let dimensionObject = await ImageDisplayManager.calculateAspectRatioFit( tex.width, tex.height, displaySceneWidth, displaySceneHeight // displayScene.data.width, // displayScene.data.height ); //scale down factor is how big the tile will be in the scene //make this scale down factor configurable at some point let scaleDownFactor = 200; dimensionObject.width -= scaleDownFactor; dimensionObject.height -= scaleDownFactor; //half of the scene's width or height is the center -- we're subtracting by half of the image's width or height to account for the offset because it's measuring from top/left instead of center //separate objects depending on the texture's dimensions -- //create an 'update' object for if the image is wide (width is bigger than height) let wideImageUpdate = { _id: displayTile.id, width: dimensionObject.width, height: dimensionObject.height, img: url, x: scaleDownFactor / 2, y: displaySceneHeight / 2 - dimensionObject.height / 2, }; //create an 'update' object for if the image is tall (height is bigger than width) let tallImageUpdate = { _id: displayTile.id, width: dimensionObject.width, height: dimensionObject.height, img: url, y: scaleDownFactor / 2, x: displaySceneWidth / 2 - dimensionObject.width / 2, }; //https://stackoverflow.com/questions/38675447/how-do-i-get-the-center-of-an-image-in-javascript //^used the above StackOverflow post to help me figure that out //Determine if the image or video is wide, tall, or same dimensions and update depending on that if (dimensionObject.height > dimensionObject.width) { //if the height is longer than the width, use the tall image object return tallImageUpdate; } else if (dimensionObject.width > dimensionObject.height) { //if the width is longer than the height, use the wide image object return wideImageUpdate; } //if the image length and width are pretty much the same, just default to the wide image update object return wideImageUpdate; } static async scaleArtTileToFrameTile(artTile, frameTile, tex, url) { const frameTileWidth = game.version >= 10 ? frameTile.width : frameTile.data.width; const frameTileHeight = game.version >= 10 ? frameTile.height : frameTile.data.height; const frameTileX = game.version >= 10 ? frameTile.x : frameTile.data.x; const frameTileY = game.version >= 10 ? frameTile.y : frameTile.data.y; let dimensionObject = ImageDisplayManager.calculateAspectRatioFit( tex.width, tex.height, frameTileWidth, frameTileHeight ); let imageUpdate = { _id: artTile.id, width: dimensionObject.width, height: dimensionObject.height, img: url, y: frameTileY, x: frameTileX, }; //Ensure image is centered to bounding tile (stops images hugging the top left corner of the bounding box). let boundingMiddle = { x: frameTileX + frameTileWidth / 2, y: frameTileY + frameTileHeight / 2, }; let imageMiddle = { x: imageUpdate.x + imageUpdate.width / 2, y: imageUpdate.y + imageUpdate.height / 2, }; imageUpdate.x += boundingMiddle.x - imageMiddle.x; imageUpdate.y += boundingMiddle.y - imageMiddle.y; return imageUpdate; } /** Used snippet from the below stackOverflow answer to help me with proportionally resizing the images*/ /*https://stackoverflow.com/questions/3971841/how-to-resize-images-proportionally-keeping-the-aspect-ratio*/ static calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) { let ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); return { width: srcWidth * ratio, height: srcHeight * ratio, }; } static async displayImageInWindow(method, url) { if (method === "journalEntry") { let dedicatedDisplayData = await HelperFunctions.getSettingValue( "artGallerySettings", "dedicatedDisplayData" ); const displayJournalID = dedicatedDisplayData.journal.value; const displayJournal = game.journal.get(displayJournalID); //if we would like to display in a dedicated journal entry if (!displayJournal) { //couldn't find display journal, so return if (game.user.isGM) { ui.notifications.error( `No Art Journal entry set! Please set your art journal in the module settings or Art Gallery Config` ); } return; } else { displayJournal.render(true); } //get the file type of the image url via regex match var fileTypePattern = /\.[0-9a-z]+$/i; var fileType = url.match(fileTypePattern); let journalMode = "image"; let update; if (fileType == ".mp4" || fileType == ".webm") { if (game.version < 10) { // if the file type is a video and we're before v10, we have to do a bit of a wonky workaround let videoHTML = `
`; update = { _id: displayJournal._id, content: videoHTML, img: "", }; imageMode = "text"; } else { //if we're after v10 update = { _id: displayJournal.id, src: url, video: { loop: true, autoplay: true, }, type: "video", }; } } else { //change the background image to be the clicked image in the journal if (game.version < 10) { update = { _id: displayJournal.id, content: "", img: url, }; } else { update = { // _id: displayJournal.id, src: url, type: "image", }; } } let updated; if (game.version < 10) { updated = await displayJournal.update(update, {}); } else { const firstPage = displayJournal.pages.contents[0]; updated = await firstPage?.update({ _id: firstPage.id, ...update }); } if (updated === null && game.user.isGM) { ui.notifications.error( `Could not display image in Art Journal '${displayJournal.name}.'` ); } const { autoActivate, autoView } = await HelperFunctions.getSettingValue( "artGallerySettings", "dedicatedDisplayData.journal" ); if (autoActivate) displayJournal.show(journalMode, true); if (autoView && !displayJournal.sheet.rendered) displayJournal.sheet.render(true); if (game.user.isGM && !autoActivate && !autoView) { ui.notifications.info( `Image in Art Journal '${displayJournal.name}' successfully updated` ); } } else if (method === "window") { //if we would like to display in a new popout window let popout = new ImageVideoPopout(url, { shareable: true, }) .render(true) .shareImage(); } } /** * determine the location of the display * @param {*} imageElement - the imageElement * @param {*} location - the location we want to display our image in * @param {*} journalSheet - the journal sheet in which we're performing these actions * @param {*} url */ static async determineDisplayMethod(sheetImageData = { method: "window", url: "" }) { let { method, imageElement, url } = sheetImageData; if (!url && imageElement) { url = ImageDisplayManager.getImageSource(imageElement); } else if (!url && !imageElement) { console.error("No image data passed to this method", url, imageElement); } //on click, this method will determine if the image should open in a scene or in a display journal switch (method) { case "artScene": let artSceneID = await HelperFunctions.getSettingValue( "artGallerySettings", "dedicatedDisplayData.scene.value" ); let artScene = game.scenes.get(artSceneID); if (artScene) { let defaultArtTileID = await ArtTileManager.getDefaultArtTileID( artScene ); if (!defaultArtTileID) { ui.notifications.error( "No valid 'Art Tile' found in scene '" + artScene.name + "'" ); return; } let frameTileID = await ArtTileManager.getGalleryTileDataFromID( defaultArtTileID, "linkedBoundingTile", artSceneID ); await ImageDisplayManager.updateTileObjectTexture( defaultArtTileID, frameTileID, url, method, artSceneID ); } break; case "anyScene": let { artTileID, frameTileID } = sheetImageData; //if the setting is to display it in a scene, proceed as normal if (method === "anyScene" && !artTileID) { artTileID = await ArtTileManager.getDefaultArtTileID( game.scenes.viewed ); if (!artTileID) { ui.notifications.error( "No valid 'Art Tile' found in current scene" ); return; } frameTileID = await ArtTileManager.getGalleryTileDataFromID( artTileID, "linkedBoundingTile" ); } await ImageDisplayManager.updateTileObjectTexture( artTileID, frameTileID, url, method ); break; case "journalEntry": case "window": await ImageDisplayManager.displayImageInWindow(method, url); break; } } static getImageSource(imageElement) { let type = imageElement.nodeName; let url; if (type == "IMG") { //if it's an image element url = imageElement.getAttribute("src"); } else if (type == "VIDEO") { //if it's a video element url = imageElement.getElementsByTagName("source")[0]?.getAttribute("src"); if (!url) { url = imageElement.getAttribute("src"); } } else if (type == "DIV" && imageElement.classList.contains("lightbox-image")) { //if it's a lightbox image on an image-mode journal //https://stackoverflow.com/questions/14013131/how-to-get-background-image-url-of-an-element-using-javascript -- let imgStyle = imageElement.style; url = imgStyle.backgroundImage.slice(4, -1).replace(/['"]/g, ""); } else { ui.notifications.error("Type not supported"); url = null; } if (!url) { ui.notifications.error("url not found"); } return url; //load the texture from the source } static async clearDisplayWindow() { if (!findDisplayJournal()) { return; } let url = "/modules/journal-to-canvas-slideshow/artwork/HD_transparent_picture.png"; let update = { _id: displayJournal.id, img: url, content: `
`, }; const updated = await displayJournal.update(update, {}); } static async clearTile(tileID, options = {}) { let { ourScene } = options; if (!ourScene) ourScene = game.scenes.viewed; const isBoundingTile = await ArtTileManager.getGalleryTileDataFromID( tileID, "isBoundingTile" ); let propertyPath = "defaultTileImages.paths.artTilePath"; if (isBoundingTile) { propertyPath = "defaultTileImages.paths.frameTilePath"; } const clearImagePath = await HelperFunctions.getSettingValue( "artGallerySettings", propertyPath ); var clearTileUpdate = { _id: tileID, img: clearImagePath, }; await ourScene.updateEmbeddedDocuments("Tile", [clearTileUpdate]); } }