Skip to content

Instantly share code, notes, and snippets.

@LoganTann
Last active September 9, 2020 15:15
Show Gist options
  • Save LoganTann/df16e6a3f8600421fedd4fd400e5a686 to your computer and use it in GitHub Desktop.
Save LoganTann/df16e6a3f8600421fedd4fd400e5a686 to your computer and use it in GitHub Desktop.
$ _Layered : layered sprites + perfect crossfading addon for Monogatari Visual Novel Library

$ _layered v0.1.01

/!\ Latest version here is 0.2.01. This file is outdated. Latest version is still not documented yet.

The idea

Even if the layered sprite function is currently being developped by monogatari's creator, we needed another feature for our game that was the perfect crossfade. In HTML, it is possible to stack multiple images in a background, but crossfading (or in other words, applying the "merge" blend mode) is still in the CSS4 drafts. Even worse, applying a transition to the background-image proprety is not in the standarts...

But the strange and cool thing is that it's available in most WebKit based browsers (at least chrome 17+), and that transition applies exactly the "merge" blend mode for the old and the new background. To summary, this is another way to add the layered sprites feature, with their pros and cons, and allow in addition some browsers to have a perfect transition when sprites needs to be changed.

Pros & Cons (Not limited list)

  • 👍 Some browsers will apply a perfect crossfade transition.
  • 👎 Currently, Layers should have the same size, or at least the same width
  • 👎 This is unstable, because it edits directly the image generated by the VN lib. If the lib haves to edit the original sprites, it might fails !
  • 👎 This technique is not standart. Transitions will not work with firefox.
  • 👎 Playing with background images is not a good way to display images...

Warning

  1. Currently, the code will work fine only for webkit-based browsers.
    Thus, Firefox can't apply fading transitions (that will be ugly, so sad, it's my everyday browser, but the sacrifice worth)...
  2. This custom action is still in beta stage. I might update the syntax ! This is not well documented ! You must master the Monogatari Lib if you wish to use this addon.

Syntax

$ _layered [characterName] [reference1] [reference2] [...referenceX] ([with] [...classes])
  • [characterName] : must have been defined both in GLOBALS.layered[] and monogatari.characters({}).
  • [referenceX] : You should add these arguments as many as total amount of layers defined. Possible values are "empty" (No need to explain), a reference of layer defined in GLOBALS.layered[<characterName>], or "-" to keep the last value defined.
  • with [...classes] : sames as used in the show character command.

And voilà !! Usage is the same as the show character command, but you replace it with $ _layered and add a reference for each layers, and you need of course to include it by copy-pasting in the main.js (I don't recommand it) or including with the script tag.

To try the code :

  • Extract and copy the files inside PNG_layers_for_testing.zip to the folder assets/characters/layered, to get some PNG layers for testing. Example code in this section will works with thoses files.

  • Copy the included main.js file to js/addons/$_layered/main.js (feel free to change the name or not, as long as it works in your project)

  • Include the script :

    <script src="js/addons/$_layered/main.js"></script>
  • Define the testing character in monogatari.characters ({});

      'test' : {
        name: 'test',
        color: '#aaaaaa',
        directory: 'layered',
        sprites: {
          default: "2-body.png",
        }
      }
  • Defines the layers of the character (paste this directly at the end of the script js/addons/$_layered/main.js)

      // parameters are defined inside a global object
      if (typeof GLOBALS !== "object" || typeof GLOBALS.layered !== "object") {
        var GLOBALS = {
          layered: {}
        }
      }
      // Define characters layers like this :  (might change if I plan to upgrade my function!)
      // GLOBALS.layered.<your character name> = [...]
      GLOBALS.layered.test = [
        { // Fallback + storage
          layersHistory: [],
          default: 'default'//must be an identifier defined with monogatari.characters
          // Because if it fails, _layered will run "show character <characterName> <default>"
        },
        { // layer 1 (front)
          handsDown: "assets/characters/layered/1-handsDown.png",
          handsUp: "assets/characters/layered/1-handsUp.png",
        },
        { // layer 2 (middle)
          body: "assets/characters/layered/2-body.png"
        },
        { // layer 3 (back)
          headNope: "assets/characters/layered/3-headNope.png",
          headYes: "assets/characters/layered/3-headYes.png"
        }
        // add others layers if needed ! but requires to add "n" more arguments for "n" numbers of layers
      ]
  • Demo script :

    script["layered_demo"] = [
      "show scene white",
    	"$ _layered test handsDown body headNope with fadeIn end-fadeOut",
    	"test I'm upset",
    	"$ _layered test handsUp - headYes with fadeIn end-fadeOut",
    	"test Now, I'm <em>not</em> upset",
    	"$ _layered test handsDown - empty with fadeIn end-fadeOut",
    	"test WTF I don't have any heads ?!",
    	"test that's it !",
    	"end"
    ];
  • Then jump to the label layered_demo !

/* $ _layered v0.2.01. Part of the Retaining's Memories game (https://kagescan.legtux.org/fangame/).
*
* FEEL FREE TO USE, FORK AND EDIT THAT CUSTOM ACTION, as long as the comment
* below stays here :
*
* The MIT License (MIT)
* Copyright (c) 2017, 2020 ShinProg / Kagescan
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// CODE ------------------------------------------------------------------------
if (! CSS.supports("( -webkit-box-reflect:unset )")){
// Checks for webkit only browser.
// !! remove this code when firefox supports transitions for background-images
// like webkit based browsers already does.
console.warn(
"%c (IMPORTANT)", "color: black; font-weight: bold;",
"_Layered warn : the perfect crossfading feature still don't work on",
"browsers that are not based on webkit ! Original crossfades with fadeIn",
"end-fadeOut classes are not even not supported.",
);
}
monogatari.$ ('_layered', async function(character, ...arguments) {
// TODO: Add default transitions for firefox !
// var init
const character_object = monogatari.character(character);
if (typeof character_object !== "object") {
throw `_Layered Error : character ${character} is not defined !`;
}
const character_sprites = character_object["sprites"];
const character_internal = character_object["$_layered"];
if (typeof character_internal !== "object" || typeof character_sprites !== "object") {
throw `_Layered Error : character ${character} don't have valid [sprites] or`
+ `[$_layered] child objects (or they don't exists.)!`;
}
if (! Array.isArray(character_internal["_history"])) {
character_internal["_history"] = [];
}
const sprite_ref_to_url = function(reference) {
// assumes that [reference] is valid !! Same with [directory] in chara object.
const img_file = character_object.sprites[reference];
const chara_dir = character_object.directory;
return `assets/characters/${chara_dir}/${img_file}`;
};
// https://stackoverflow.com/a/10262019
const is_empty = (string) => (! string.replace(/\s/g, '').length);
// Getting classes and max nbr of layers.
let classes = "";
let max_nbr_of_layers = arguments.length;
for (let i = 0; i < arguments.length; i++) {
if (i >= max_nbr_of_layers) {
classes += arguments[i] + " ";
}
if (arguments[i] == "with") {
max_nbr_of_layers = i;
} else if (is_empty(arguments[i])) {
console.warn(`_Layered Warn : Argument ${i+1} is empty. This might cause unexpected behaviors from the custom action !`);
}
}
// checking if this is the first time the character appears in the scene
if (document.querySelector(`img[data-character="${character}"]`) === null) {
const defaultSprite = character_internal["default"];
if ( typeof defaultSprite !== "string"
|| typeof character_object.sprites[defaultSprite] !== "string") {
throw `_Layered Error : default sprite for character ${character} is undefined or not valid`;
}
await monogatari.run(`show character ${character} ${defaultSprite} ${classes}`);
}
// checking if the code in the "if" just above worked
const image = document.querySelector(`img[data-character="${character}"]`);
if (image === null) {
throw `_Layered Error : unable to get the image element of the [${character}] character`;
}
// Checking if the image element displays the default or an empty image.
if (! image.src.includes("data:image")) {
await new Promise( function(resolve, reject) {
let stillWaiting = true;
image.addEventListener("load", () => { stillWaiting = false; resolve() });
setTimeout( () => {
if (stillWaiting)
reject(`_Layered Error : Image loading timeout for character [${character}]`);
}, 1000);
});
if ( typeof image.naturalHeight !== "number"
|| typeof image.naturalWidth !== "number") {
throw `_Layered Error : cannot get natural height or natural width of the [${character}] image`;
}
if (image.naturalHeight <=0 || image.naturalWidth <=0) {
throw `_Layered Error : default image for character [${character}] is not valid (or should have a size greater than 0)`;
}
image.src =`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'
height='${image.naturalHeight}' width='${image.naturalWidth}'%3E%3C/svg%3E`;
}
//
let generatedBackgroundImage = "";
for (let i = 0; i < max_nbr_of_layers; i++) {
const reference = arguments[i];
let urlToAdd = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='${image.naturalHeight}' width='${image.naturalWidth}'%3E%3C/svg%3E`;
// internal function to get/set the last reference :
const last_ref_of_this_layer = function(setter) {
const last_ref = character_internal["_history"][i];
// getter
if (typeof setter === "undefined") {
if ( typeof last_ref === "string"
&& typeof character_object.sprites[last_ref] === "string") {
return last_ref;
} else {
throw `_Layered Warning : while trying to get the last reference of layer ${i}, the value obtained is not defined or not valid.`;
}
}
// setter
if (typeof character_object.sprites[setter] !== "string") {
// If you can get this error, congrats ! You're a champion !
throw `_Layered Warning : while trying to set the last reference of layer ${i}, the value obtained (${setter}) is not valid. Setting aborted.`;
}
character_internal["_history"][i] = setter;
}
// stuff
if (typeof character_object.sprites[reference] !== "string") {
try {
if (typeof reference !== "string" || is_empty(reference)) {
throw `_Layered Warning : argument ${i} is an empty reference ! Prefers set the reference to "-" or "empty" instead of nothing.\n`
+ `Or maybe you inserted an extra space. Make sure to separate each arguments with one and only one space ! \n`;
}
if (reference === "-") {
const last_ref = last_ref_of_this_layer();
urlToAdd = sprite_ref_to_url(last_ref);
} else if (reference !== "empty") {
throw `_Layered Warning : the reference of sprite image [${reference}] isn't defined or not valid ! \n`;
} // else keep that layer empty
} catch (e) {
console.warn(e, `_Layered will display the layer ${i} transparent.`);
}
} else {
urlToAdd = sprite_ref_to_url(reference);
last_ref_of_this_layer(reference);
}
// above : fallback image
if (generatedBackgroundImage !== "") {
generatedBackgroundImage += ",";
}
generatedBackgroundImage += `url("${urlToAdd}")`;
}
image.style.transition = "background-image 0.5s linear";
image.style.backgroundPosition = "center top";
image.style.backgroundRepeat = "no-repeat";
image.style.backgroundSize = "contain";
if (generatedBackgroundImage.includes("url")) {
image.style.backgroundImage = generatedBackgroundImage;
} else {
console.error("generated background image don't have valid values : ", generatedBackgroundImage);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment