diff --git a/.gitignore b/.gitignore index 37e1e056e527bdc2fca77542fd35dee10f111b04..7243b96427cd1dee4410b058a550a2ecf6537442 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ bundle.js *.map *.css.map -src/**/*.css +*.css node_modules target .env diff --git a/Makefile b/Makefile index 637aa2cb19cd0a1dd1469d9a330b57a42327df8b..61dd6ea492e6d26eb24371a49b0c9bcd6335f5e7 100644 --- a/Makefile +++ b/Makefile @@ -23,10 +23,10 @@ bash-api: docker exec -it kuadrado_server bash build-website: - npm run --prefix ./website build + npm run --prefix ./website build-prod build-website-debug: - npm run --prefix ./website build debug + npm run --prefix ./website build build-admin: npm run --prefix ./admin-frontend build diff --git a/admin-frontend/src/components/create-article-form.js b/admin-frontend/src/components/create-article-form.js index e7651c0d4883e24c905110f162e39dda1e21f11a..494687dfe9d13555cd1a693eddb6753b6b74a08a 100644 --- a/admin-frontend/src/components/create-article-form.js +++ b/admin-frontend/src/components/create-article-form.js @@ -193,7 +193,7 @@ class CreateArticleForm { maxWidth: "800px", }, contents: [ - { tag: "button", contents: "RESET", onclick: this.reset.bind(this) }, + !this.params.data && { tag: "button", contents: "RESET", onclick: this.reset.bind(this) }, { tag: "h2", contents: article.title }, { tag: "h4", contents: article.subtitle }, { tag: "p", contents: article.body.replace(/\n/g, "<br>") }, diff --git a/admin-frontend/src/components/update-article-form.js b/admin-frontend/src/components/update-article-form.js index 67994d3fdf82a9e56da2172634b363e4e996f7b6..967c4d04bdcbb9cc1faa883c127fe7ae9f81dd2b 100644 --- a/admin-frontend/src/components/update-article-form.js +++ b/admin-frontend/src/components/update-article-form.js @@ -9,7 +9,12 @@ class UpdateArticleForm { this.state = { search_article_title: "", article_to_update: {}, - } + }; + + this.articles_list = new ArticleList({ + on_select_article: this.handle_select_article.bind(this), + on_delete_result: this.handle_delete_article.bind(this) + }); } reset() { @@ -39,38 +44,6 @@ class UpdateArticleForm { .catch(err => alert(err)) } - refresh_search_result() { - obj2htm.subRender( - this.render_search_result(), - document.getElementById("update-article-form-search-result"), - { mode: "replace" }, - ); - } - - render_search_result() { - const { search_result } = this.state; - return { - tag: "div", - id: "update-article-form-search-result", - style_rules: { - display: "flex", - gap: "10px", - alignItems: "center" - }, - contents: search_result.title ? [ - { tag: "strong", contents: search_result.title }, - { - tag: "button", contents: "SELECT", - onclick: this.handle_select_result.bind(this) - }, - { - tag: "button", contents: "DELETE", - onclick: this.handle_delete_article.bind(this) - } - ] : [] - } - } - refresh_update_form() { obj2htm.subRender( this.render_update_form(), @@ -88,7 +61,7 @@ class UpdateArticleForm { data: this.state.article_to_update, on_article_sent: () => { this.reset(); - this.refresh_search_result(); + this.articles_list.refresh_list(); } }).render()] : [] @@ -110,10 +83,7 @@ class UpdateArticleForm { maxWidth: "800px", }, contents: [ - new ArticleList({ - on_select_article: this.handle_select_article.bind(this), - on_delete_result: this.handle_delete_article.bind(this) - }).render(), + this.articles_list.render(), { tag: "hr", style_rules: { width: "100%" } }, this.render_update_form(), ] diff --git a/public/assets/images/screen_mentalo_app.png b/public/assets/images/screen_mentalo_app.png new file mode 100644 index 0000000000000000000000000000000000000000..5220d25f95dba8e4d2c5fdb3f45a4cd9fcff4931 Binary files /dev/null and b/public/assets/images/screen_mentalo_app.png differ diff --git a/public/assets/translations/en.json b/public/assets/translations/en.json index 7dbe0401ad090f13686d140bc20ce64410f3a334..ad1ff38773a36f1bc9069bd8058aebdaa7d66f0c 100644 --- a/public/assets/translations/en.json +++ b/public/assets/translations/en.json @@ -29,5 +29,7 @@ "edu-learn-mentalo": "Create a game in a few sessions with the Mentalo application. Handle logical, narrative and artistic concepts with maximum simplicity.", "Me contacter": "Contact me", "Pour s'inscrire ou en savoir plus": "To register or find out more", - "software-page-intro": "R&D, experimental projects, software tools for game development or for the web." + "software-page-intro": "R&D, experimental projects, software tools for game development or for the web.", + "Programme XXXX": "{%date%} program", + "Rien de prévu pour le moment": "Nothing planned for the moment" } \ No newline at end of file diff --git a/public/assets/translations/fr.json b/public/assets/translations/fr.json index fdcaa31909af1067f2f13dbe733496b97e59ed30..b2aea9cb2fa0ff62530b6a7e93aabd8aa9076047 100644 --- a/public/assets/translations/fr.json +++ b/public/assets/translations/fr.json @@ -12,5 +12,6 @@ "edu-learn-computer": "Perdu avec votre ordinateur ou votre smartphone, les logiciels, internet ? Prenez en main les fondamentaux apprenez pas à pas à utiliser sereinement la technologie.", "edu-learn-gnu": "<b>Passez le cap du libre ! </b><br/>Apprenez à installer Linux, faites vos premiers pas avec les logiciels libres et acquérez une autonomie suffisante pour une utilisation basique.", "edu-learn-mentalo": "Créez un jeu en quelques séances avec l'application Mentalo. Manipulez des concepts logiques, narratifs et artistiques avec le maximum de simplicité.", - "software-page-intro": "R&D, projets expérimentaux, outillage logiciel pour le développement de jeu ou pour le web." + "software-page-intro": "R&D, projets expérimentaux, outillage logiciel pour le développement de jeu ou pour le web.", + "Programme XXXX": "Programme {%date%}" } \ No newline at end of file diff --git a/public/education/education.js b/public/education/education.js index 8207c7e067fad0161f321ccd291908f1a0f00a0d..566910037b310b1c3156d9fe2e7923594227f527 100644 --- a/public/education/education.js +++ b/public/education/education.js @@ -1,879 +1,2 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ -module.exports = { - build: { - protected_dirs: ["assets", "style", "views", "standard"], - default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"], - }, -}; - -},{}],2:[function(require,module,exports){ -module.exports = { - images_url: `/assets/images`, - data_url: `/assets/data`, - translations_url: "/assets/translations" -}; - -},{}],3:[function(require,module,exports){ -"use strict"; -/** - * A tiny library to handle text translation. - */ - -/** - * init parameter object type - * @typedef TranslatorParam - * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes - * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" - * - * @property {Boolean} use_url_locale_fragment - DEFAULT: true - - * If true, the 1st fragment of the window.location url will be parsed as a language code - * Example: - * window.location - * > https://example.com/en/some-page... - * then the /en/ part of the url will be taken in priority and locale will be set to "en". - * window.location - * > https://example.com/some-page... - * No locale fragment will be found so locale will not be set from url fragment. - * window.location - * > https://example.com/some-page/en - * Doesn't work, locale fragment must be the first framgment - * - * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" - * The key used to saved the current locale into local storage - * - * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations - * Translations files are expected to be named with their corresponding locale code. - * Example: - * if supported_languages is set to ["en", "fr", "it"] - * and translations_url is set to "https://example.com/translations/"" - * Then the expected translations files are - * https://example.com/translations/en.json - * https://example.com/translations/fr.json - * https://example.com/translations/it.json - * The json resources must simple key value maps, value being the translated text. - */ - -module.exports = { - locale: "en", // ISO 639 lowercase language code - supported_languages: ["en"], - translations: {}, - translations_url: "", - use_url_locale_fragment: true, - local_storage_key: "translator-prefered-language", - - /** - * Initialize the lib with params - * @param {TranslatorParam} params - * @returns {Promise} - */ - init(params) { - Object.entries(params).forEach(k_v => { - const [key, value] = k_v; - if ([ - "supported_languages", - "use_url_locale_fragment", - "local_storage_key", - "translations_url" - ].includes(key)) { - this[key] = value; - } - }); - - this.translations_url = this.format_translations_url(this.translations_url); - this.supported_languages = this.format_supported_languages(this.supported_languages); - - return new Promise((resolve, reject) => { - const loc = - (() => {// Locale from url priority 1 - if (this.use_url_locale_fragment) { - const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - return fragment_is_locale ? first_url_fragment : ""; - } else { - return ""; - } - })() - || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 - || (() => { // locale from navigator priority 3 - const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); - return this.supported_languages.includes(navigator_locale) - ? navigator_locale - : this.supported_languages[0] // Default if navigator locale is not supported - })(); - - fetch(`${this.translations_url}${loc}.json`) - .then(response => response.json()) - .then(response => { - this.locale = loc; - this.translations = response; - resolve(); - }) - .catch(err => { - this.locale = "en"; - reject(err); - }); - }); - }, - - /** - * Return a lowercase string without dash. - * If given locale is en-EN, then "en" will be returned. - * @param {String} locale - * @returns A lowercase string - */ - format_locale(locale) { - return locale.split("-")[0].toLowerCase(); - }, - - /** - * Appends a slash at the end of the given string if missing, and returns the url. - * @param {String} url - * @returns {String} - */ - format_translations_url(url) { - if (url.charAt(url.length - 1) !== "/") { - url += "/" - } - return url; - }, - - /** - * Return the array of language codes formatted as lowsercase language code. - * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. - * @param {String[]} languages_codes - * @returns {String[]} - */ - format_supported_languages(languages_codes) { - return languages_codes.map(lc => this.format_locale(lc)); - }, - - /** - * Fetches a new set of translations in case the wanted language changes - * @param {String} locale A lowercase language code - * @returns {Promise} - */ - update_translations(locale) { - locale = this.format_locale(locale); - return new Promise((resolve, reject) => { - fetch(`${this.translations_url}${locale}.json`) - .then(response => response.json()) - .then(response => { - this.translations = response; - this.locale = locale; - - localStorage.setItem(this.local_storage_key, locale); - - const split_path = window.location.pathname.substring(1).split("/"); - const first_url_fragment = split_path[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - - if (fragment_is_locale) { - split_path.splice(0, 1, locale); - const updated_path = split_path.join("/"); - window.history.replaceState(null, "", "/" + updated_path); - } - resolve(); - }) - .catch(err => { - reject(err); - }); - }); - }, - - /** - * Tries to get the translation of the source string, or return the string as it. - * @param {String} text The source text to translate - * @param {Object} params Some dynamic element to insert in the text - * Params can be used if the translated text provide placeholder like {%some_word%} - * Example: - * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) - * -> translation for "Some trad key": "A translated text with {%some_word%}" - * -> will be return as : "A translated text with a dynamic parameter" - * @returns {String} - */ - trad: function (text, params = {}) { - text = this.translations[text] || text; - - Object.keys(params).forEach(k => { - text = text.replace(`{%${k}%}`, params[k]); - }); - - return text; - } -}; -},{}],4:[function(require,module,exports){ -"use strict"; - -module.exports = { - register_key: "objectToHtmlRender", - - /** - * Register "this" as a window scope accessible variable named by the given key, or default. - * @param {String} key - */ - register(key) { - const register_key = key || this.register_key; - window[register_key] = this; - }, - - /** - * This must be called before any other method in order to initialize the lib. - * It provides the root of the rendering cycle as a Javascript object. - * @param {Object} renderCycleRoot A JS component with a render method. - */ - setRenderCycleRoot(renderCycleRoot) { - this.renderCycleRoot = renderCycleRoot; - }, - - event_name: "objtohtml-render-cycle", - - /** - * Set a custom event name for the event that is trigger on render cycle. - * Default is "objtohtml-render-cycle". - * @param {String} evt_name - */ - setEventName(evt_name) { - this.event_name = evt_name; - }, - - /** - * This is the core agorithm that read an javascript Object and convert it into an HTML element. - * @param {Object} obj The object representing the html element must be formatted like: - * { - * tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li... - * xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag. - * See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information - * style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax, - * like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc. - * contents: Array or String // This reprensents the contents that will be nested in the created html element. - * <div>{contents}</div> - * The contents can be an array of other objects reprenting elements (with tag, contents, etc) - * or it can be a simple string. - * // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title... - * // or they can also define custom html5 attributes, like data, my_custom_attr or anything. - * } - * @returns {HTMLElement} The output html node. - */ - objectToHtml(obj) { - if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process. - const objectToHtml = this.objectToHtml.bind(this); - const { tag, xmlns } = obj; - const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag); - const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"]; - - Object.keys(obj) - .filter(attr => !excludeKeys.includes(attr)) - .forEach(attr => { - switch (attr) { - case "class": - node.classList.add(...obj[attr].split(" ").filter(s => s !== "")); - break; - case "on_render": - if (!obj.id) { - node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`; - } - if (typeof obj.on_render !== "function") { - console.error("The on_render attribute must be a function") - } else { - this.attach_on_render_callback(node, obj.on_render); - } - break; - default: - if (xmlns !== undefined) { - node.setAttributeNS(null, attr, obj[attr]) - } else { - node[attr] = obj[attr]; - } - } - }); - if (obj.contents && typeof obj.contents === "string") { - node.innerHTML = obj.contents; - } else { - obj.contents && - obj.contents.length > 0 && - obj.contents.forEach(el => { - switch (typeof el) { - case "string": - node.innerHTML = el; - break; - case "object": - if (xmlns !== undefined) { - el = Object.assign(el, { xmlns }) - } - node.appendChild(objectToHtml(el)); - break; - } - }); - } - - if (obj.style_rules) { - Object.keys(obj.style_rules).forEach(rule => { - node.style[rule] = obj.style_rules[rule]; - }); - } - - return node; - }, - - on_render_callbacks: [], - - /** - * This is called if the on_render attribute of a component is set. - * @param {HTMLElement} node The created html element - * @param {Function} callback The callback defined in the js component to render - */ - attach_on_render_callback(node, callback) { - const callback_handler = { - callback: e => { - if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) { - callback(node); - const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node))); - if (handler_index === -1) { - console.warn("A callback was registered for node with id " + node.id + " but callbacck handler is undefined.") - } else { - window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback) - this.on_render_callbacks.splice(handler_index, 1); - } - } - }, - node, - }; - - const len = this.on_render_callbacks.push(callback_handler); - window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback); - }, - - /** - * If a main element exists in the html document, it will be used as rendering root. - * If not, it will be created and inserted. - */ - renderCycle: function () { - const main_elmt = document.getElementsByTagName("main")[0] || (function () { - const created_main = document.createElement("main"); - document.body.appendChild(created_main); - return created_main; - })(); - - this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" }); - }, - - /** - * This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component, - * it can start from any component of the tree. The root component must be given as the first argument, the second argument be - * be a valid html element in the dom and will be used as the insertion target. - * @param {Object} object An object providing a render method returning an object representation of the html to insert - * @param {HTMLElement} htmlNode The htlm element to update - * @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override", - * "insert-before" (must be defined along with an insertIndex key (integer)), - * "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove". - * In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}... - */ - subRender(object, htmlNode, options = { mode: "append" }) { - let outputNode = null; - - const get_insert = () => { - outputNode = this.objectToHtml(object); - return outputNode; - }; - - switch (options.mode) { - case "append": - htmlNode.appendChild(get_insert()); - break; - case "override": - htmlNode.innerHTML = ""; - htmlNode.appendChild(get_insert()); - break; - case "insert-before": - htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]); - break; - case "adjacent": - /** - * options.insertLocation must be one of: - * - * afterbegin - * afterend - * beforebegin - * beforeend - */ - htmlNode.insertAdjacentHTML(options.insertLocation, get_insert()); - break; - case "replace": - htmlNode.parentNode.replaceChild(get_insert(), htmlNode); - break; - case "remove": - htmlNode.remove(); - break; - } - const evt_name = this.event_name; - const event = new CustomEvent(evt_name, { - detail: { - inputObject: object, - outputNode, - insertOptions: options, - targetNode: htmlNode, - } - }); - - window.dispatchEvent(event); - }, -}; -},{}],5:[function(require,module,exports){ -"use strict"; -const translator = require("ks-cheap-translator"); -const { translations_url } = require("../../constants"); - -class WebPage { - constructor(args) { - Object.assign(this, args); - - if (!this.id) { - this.id = "webpage-" + performance.now(); - } - - translator.init({ - translations_url, - supported_languages: ["fr", "en"], - }).then(this.refresh_all.bind(this)); - } - - refresh() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) - } - - refresh_all() { - obj2htm.renderCycle() - } -} - -module.exports = WebPage; -},{"../../constants":2,"ks-cheap-translator":3}],6:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const WebPage = require("../../lib/web-page"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -const EDU_THEMES = [ - { - title: "Programmation", - description: "edu-learn-coding", - image: "learning_theme_coding.png", - }, - { - title: "Dessin numérique et animation 2D", - description: "edu-learn-2d", - image: "learning_theme_2d.png", - }, - { - title: "Maths et physique", - description: "edu-learn-math", - image: "learning_theme_math.png", - }, - { - title: "Aide informatique générale", - description: "edu-learn-computer", - image: "learning_theme_pc.png", - }, - { - title: "Stage GNU/Linux", - description: "edu-learn-gnu", - image: "learning_theme_linux.png" - }, - { - title: "Créer un jeu avec Mentalo", - description: "edu-learn-mentalo", - image: "learning_theme_mentalo.png", - } -]; - -class EducationPage extends WebPage { - render() { - return { - tag: "div", - id: "education-page", - typeof: "EducationalOrganization", - contents: [ - { - tag: "div", - class: "page-header logo-left", - contents: [ - { - tag: "div", - class: "page-contents-center grid-wrapper", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: "image brain", - src: `${images_url}/brain.svg`, - }, - ], - }, - { tag: "h1", contents: t("Pédagogie") }, - { - tag: "p", - contents: t("edu-page-intro"), - }, - ], - }, - ], - }, - { - tag: "div", - class: "title-banner", - }, - { - tag: "section", - class: "bg-dark", - contents: [ - { - tag: "div", - class: "page-contents-center", - contents: [ - { - tag: "ul", - class: "edu-themes", - contents: EDU_THEMES.map(theme => { - return { - tag: "li", - class: "edu-theme", - contents: [ - { tag: "img", width: 250, height: 140, class: "pixelated", src: `${images_url}/${theme.image}` }, - { tag: "h3", contents: t(theme.title) }, - { tag: "p", contents: t(theme.description) }, - ] - } - }) - }, - ] - }, - ] - }, - { - tag: "section", - class: "practical-info", - contents: [ - { - tag: "div", - class: "page-contents-center", - contents: [ - { - tag: "div", - class: "info-block", - contents: [ - { tag: "h3", class: "info-title", contents: `${t("Pour s'inscrire ou en savoir plus")} <em>(programme 2022 à définir, plus d'infos bientôt)</em>` }, - { - tag: "ul", - class: "info-body", - contents: [ - { - tag: "li", - contents: [ - { tag: "span", contents: t("Me contacter") }, - { - tag: "a", - href: "mailto:contact@kuadrado-software.fr", - contents: "contact@kuadrado-software.fr", - } - ] - }, - { - tag: "li", - contents: [ - { tag: "span", contents: "" }, - { - tag: "a", - href: "tel:+33475780872", - contents: "04 75 78 08 72", - property: "telephone", - }, - ] - }, - ] - } - ] - } - ] - } - ] - - }, - ], - }; - } -} - -module.exports = EducationPage; - -},{"../../../constants":2,"../../lib/web-page":5,"ks-cheap-translator":3}],7:[function(require,module,exports){ -"use strict"; -const runPage = require("../../run-page"); -const EducationPage = require("./education"); -runPage(EducationPage); - -},{"../../run-page":8,"./education":6}],8:[function(require,module,exports){ -"use strict"; - -const renderer = require("object-to-html-renderer") -const Template = require("./template/template"); - -module.exports = function runPage(PageComponent) { - const template = new Template({ page: new PageComponent() }); - renderer.register("obj2htm") - obj2htm.setRenderCycleRoot(template); - obj2htm.renderCycle(); -}; - -},{"./template/template":10,"object-to-html-renderer":4}],9:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -const NAV_MENU_ITEMS = [ - { url: "/games/", text: "Jeux" }, - { - url: "/education/", - text: "Pédagogie", - }, - { url: "/software-development/", text: "Software" } -]; - -class NavBar { - constructor() { - this.initEventHandlers(); - } - - handleBurgerClick() { - document.getElementById("nav-menu-list").classList.toggle("responsive-show"); - } - - initEventHandlers() { - window.addEventListener("click", event => { - if ( - event.target.id !== "nav-menu-list" && - !event.target.classList.contains("burger") && - !event.target.parentNode.classList.contains("burger") - ) { - document.getElementById("nav-menu-list").classList.remove("responsive-show"); - } - }); - } - - handle_chang_lang(lang) { - translator.update_translations(lang).then(() => { - obj2htm.renderCycle(); - }).catch(err => console.log(err)); - } - - renderHome() { - return { - tag: "div", - class: "home", - contents: [ - { - tag: "a", - href: "/", - contents: [ - { - tag: "img", - alt: "Logo Kuadrado", - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - alt: "Kuadrado Software", - class: "logo-text", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - ], - }; - } - - renderMenu(menuItemsArray, isSubmenu = false, parentUrl = "") { - return { - tag: "ul", - id: "nav-menu-list", - class: isSubmenu ? "submenu" : "", - contents: menuItemsArray.map(item => { - const { url, text, submenu } = item; - const href = `${parentUrl}${url}`; - return { - tag: "li", - class: !isSubmenu && window.location.pathname === href ? "active" : "", - contents: [ - { - tag: "a", - href, - contents: t(text), - }, - ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), - }; - }).concat({ - tag: "li", - class: "lang-flags", - contents: ["fr", "en"].map(lang => { - return { - tag: "img", src: `${images_url}/flag-${lang}.svg`, - class: translator.locale === lang ? "selected" : "", - onclick: this.handle_chang_lang.bind(this, lang) - } - }) - }), - }; - } - - renderResponsiveBurger() { - return { - tag: "div", - class: "burger", - onclick: this.handleBurgerClick.bind(this), - contents: [{ tag: "span", contents: "···" }], - }; - } - - render() { - return { - tag: "nav", - contents: [ - this.renderHome(), - this.renderResponsiveBurger(), - this.renderMenu(NAV_MENU_ITEMS), - ], - }; - } -} - -module.exports = NavBar; - -},{"../../../constants":2,"ks-cheap-translator":3}],10:[function(require,module,exports){ -"use strict"; - -const { in_construction } = require("../../config"); -const { images_url } = require("../../constants"); -const NavBar = require("./components/navbar"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator) - -class Template { - constructor(props) { - this.props = props; - } - render() { - return { - tag: "main", - contents: [ - { - tag: "header", - contents: [new NavBar().render()], - }, - in_construction && { - tag: "section", - class: "warning-banner", - contents: [ - { - tag: "strong", - class: "page-contents-center", - contents: t("Site en construction ..."), - }, - ], - }, - { - tag: "section", - id: "page-container", - contents: [this.props.page.render()], - }, - { - tag: "footer", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: `logo Kuadrado`, - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - class: "text-logo", - alt: "Kuadrado Software", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - { - tag: "span", - contents: "32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France", - }, - { - tag: "div", - contents: [ - { tag: "strong", contents: "<blue>Contact : </blue>" }, - { - tag: "a", - href: "mailto:contact@kuadrado-software.fr", - contents: "contact@kuadrado-software.fr", - }, - ], - }, - { - tag: "div", - class: "social", - contents: [ - { - tag: "strong", - contents: `<blue>${t("Sur les réseaux")} : </blue>`, - }, - { - tag: "a", - href: "https://www.linkedin.com/company/kuadrado-software", - target: "_blank", - contents: "in", - title: "Linkedin", - }, - { - tag: "a", - href: "https://mastodon.gamedev.place/@kuadrado_software", - target: "_blank", - contents: "m", - title: "Mastodon", - } - ], - }, - { - tag: "span", - contents: `Copyleft 🄯 ${new Date() - .getFullYear()} Kuadrado Software | - ${t("kuadrado-footer-copyleft")}`, - }, - { - tag: "div", contents: [ - { tag: "span", contents: t("Ce site web est") + " " }, - { - tag: "a", target: "_blank", - style_rules: { fontWeight: "bold" }, - href: "https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md", - contents: "OPEN SOURCE" - } - ] - } - ], - }, - ], - }; - } -} - -module.exports = Template; - -},{"../../config":1,"../../constants":2,"./components/navbar":9,"ks-cheap-translator":3}]},{},[7]); +!function s(a,r,o){function i(t,e){if(!r[t]){if(!a[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=r[t]={exports:{}},a[t][0].call(n.exports,function(e){return i(a[t][1][e]||e)},n,n.exports,s,a,r,o)}return r[t].exports}for(var c="function"==typeof require&&require,e=0;e<o.length;e++)i(o[e]);return i}({1:[function(e,t,n){t.exports={build:{protected_dirs:["assets","style","views","standard"],default_meta_keys:["title","description","image","open_graph","json_ld"]}}},{}],2:[function(e,t,n){t.exports={images_url:"/assets/images",data_url:"/assets/data",translations_url:"/assets/translations"}},{}],3:[function(e,t,n){"use strict";t.exports={locale:"en",supported_languages:["en"],translations:{},translations_url:"",use_url_locale_fragment:!0,local_storage_key:"translator-prefered-language",init(e){return Object.entries(e).forEach(e=>{var[t,e]=e;["supported_languages","use_url_locale_fragment","local_storage_key","translations_url"].includes(t)&&(this[t]=e)}),this.translations_url=this.format_translations_url(this.translations_url),this.supported_languages=this.format_supported_languages(this.supported_languages),new Promise((t,n)=>{const s=(()=>{if(this.use_url_locale_fragment){var e=window.location.pathname.substring(1).split("/")[0];return this.supported_languages.includes(e)?e:""}return""})()||localStorage.getItem(this.local_storage_key)||(e=navigator.language.split("-")[0].toLocaleLowerCase(),this.supported_languages.includes(e)?e:this.supported_languages[0]);var e;fetch(`${this.translations_url}${s}.json`).then(e=>e.json()).then(e=>{this.locale=s,this.translations=e,t()}).catch(e=>{this.locale="en",n(e)})})},format_locale(e){return e.split("-")[0].toLowerCase()},format_translations_url(e){return"/"!==e.charAt(e.length-1)&&(e+="/"),e},format_supported_languages(e){return e.map(e=>this.format_locale(e))},update_translations(s){return s=this.format_locale(s),new Promise((n,t)=>{fetch(`${this.translations_url}${s}.json`).then(e=>e.json()).then(e=>{this.translations=e,this.locale=s,localStorage.setItem(this.local_storage_key,s);const t=window.location.pathname.substring(1).split("/");e=t[0];this.supported_languages.includes(e)&&(t.splice(0,1,s),e=t.join("/"),window.history.replaceState(null,"","/"+e)),n()}).catch(e=>{t(e)})})},trad:function(t,n={}){return t=this.translations[t]||t,Object.keys(n).forEach(e=>{t=t.replace(`{%${e}%}`,n[e])}),t}}},{}],4:[function(e,t,n){"use strict";t.exports={register_key:"objectToHtmlRender",register(e){e=e||this.register_key;window[e]=this},setRenderCycleRoot(e){this.renderCycleRoot=e},event_name:"objtohtml-render-cycle",setEventName(e){this.event_name=e},objectToHtml(t){if(!t)return document.createElement("span");const n=this.objectToHtml.bind(this),{tag:e,xmlns:s}=t,a=void 0!==s?document.createElementNS(s,e):document.createElement(e),r=["tag","contents","style_rules","state","xmlns"];return Object.keys(t).filter(e=>!r.includes(e)).forEach(e=>{switch(e){case"class":a.classList.add(...t[e].split(" ").filter(e=>""!==e));break;case"on_render":t.id||(a.id=`${btoa(JSON.stringify(t).slice(0,127)).replace(/\=/g,"")}${window.performance.now()}`),"function"!=typeof t.on_render?console.error("The on_render attribute must be a function"):this.attach_on_render_callback(a,t.on_render);break;default:void 0!==s?a.setAttributeNS(null,e,t[e]):a[e]=t[e]}}),t.contents&&"string"==typeof t.contents?a.innerHTML=t.contents:t.contents&&0<t.contents.length&&t.contents.forEach(e=>{switch(typeof e){case"string":a.innerHTML=e;break;case"object":void 0!==s&&(e=Object.assign(e,{xmlns:s})),a.appendChild(n(e))}}),t.style_rules&&Object.keys(t.style_rules).forEach(e=>{a.style[e]=t.style_rules[e]}),a},on_render_callbacks:[],attach_on_render_callback(t,n){var e={callback:e=>{e.detail.outputNode!==t&&!e.detail.outputNode.querySelector(`#${t.id}`)||(n(t),-1===(e=this.on_render_callbacks.indexOf(this.on_render_callbacks.find(e=>e.node===t)))?console.warn("A callback was registered for node with id "+t.id+" but callbacck handler is undefined."):(window.removeEventListener(this.event_name,this.on_render_callbacks[e].callback),this.on_render_callbacks.splice(e,1)))},node:t},e=this.on_render_callbacks.push(e);window.addEventListener(this.event_name,this.on_render_callbacks[e-1].callback)},renderCycle:function(){var e,e=document.getElementsByTagName("main")[0]||(e=document.createElement("main"),document.body.appendChild(e),e);this.subRender(this.renderCycleRoot.render(),e,{mode:"replace"})},subRender(e,t,n={mode:"append"}){let s=null;var a=()=>(s=this.objectToHtml(e),s);switch(n.mode){case"append":t.appendChild(a());break;case"override":t.innerHTML="",t.appendChild(a());break;case"insert-before":t.insertBefore(a(),t.childNodes[n.insertIndex]);break;case"adjacent":t.insertAdjacentHTML(n.insertLocation,a());break;case"replace":t.parentNode.replaceChild(a(),t);break;case"remove":t.remove()}var r=this.event_name,r=new CustomEvent(r,{detail:{inputObject:e,outputNode:s,insertOptions:n,targetNode:t}});window.dispatchEvent(r)}}},{}],5:[function(e,t,n){"use strict";t.exports=class{constructor(e){this.props=e,this.id=this.props.images.join("").replace(/\s\./g),this.state={showImageIndex:0},this.RUN_INTERVAL=5e3,1<this.props.images.length&&this.run()}run(){this.runningInterval=setInterval(()=>{var{showImageIndex:e}=this.state,{images:t}=this.props;this.state.showImageIndex=e<t.length-1?++e:0,this.refreshImage()},this.RUN_INTERVAL)}setImageIndex(e){clearInterval(this.runningInterval),this.state.showImageIndex=e,this.refreshImage()}refreshImage(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}render(){const{showImageIndex:n}=this.state,{images:e}=this.props;return{tag:"div",id:this.id,class:"image-carousel",contents:[{tag:"img",property:"image",alt:`image carousel ${e[n].replace(/\.[A-Za-z]+/,"")}`,src:e[n]},1<e.length&&{tag:"div",class:"carousel-bullets",contents:e.map((e,t)=>{return{tag:"span",class:`bullet ${n===t?"active":""}`,onclick:this.setImageIndex.bind(this,t)}})}]}}}},{}],6:[function(e,t,n){"use strict";const{fetch_json_or_error_text:s}=e("./fetch");t.exports={loadArticles:function(e,t){return s(`/articles/${e}/${t}`)},getArticleBody:function(e){return e.replaceAll("\n","<br/>")},getArticleDate:function(e){return`${e.getDate()}-${e.getMonth()+1}-${e.getFullYear()}`}}},{"./fetch":7}],7:[function(e,t,n){"use strict";t.exports={fetchjson:function(e){return new Promise((t,n)=>{fetch(e).then(e=>e.json()).then(e=>t(e)).catch(e=>n(e))})},fetchtext:function(e){return new Promise((t,n)=>{fetch(e).then(e=>e.text()).then(e=>t(e)).catch(e=>n(e))})},fetch_json_or_error_text:async function(e,s={}){return new Promise((t,n)=>{fetch(e,s).then(async e=>{400<=e.status&&e.status<600?n(await e.text()):t(await e.json())})})}}},{}],8:[function(e,t,n){"use strict";const s=e("ks-cheap-translator"),{translations_url:a}=e("../../constants");t.exports=class{constructor(e){Object.assign(this,e),this.id||(this.id="webpage-"+performance.now()),s.init({translations_url:a,supported_languages:["fr","en"]}).then(this.refresh_all.bind(this))}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}refresh_all(){obj2htm.renderCycle()}}},{"../../constants":2,"ks-cheap-translator":3}],9:[function(e,t,n){"use strict";const{images_url:r}=e("../../../../constants"),o=e("../../../generic-components/image-carousel"),{getArticleBody:i}=e("../../../lib/article-utils");t.exports=class{constructor(e){this.props=e}render(){const{title:e,body:t,subtitle:n,images:s,details:a=[]}=this.props;return{tag:"article",class:"edu-article",typeof:"Article",contents:[{tag:"h2",class:"edu-art-title",contents:e,property:"name"},{tag:"div",class:"edu-art-image",contents:[{tag:"img",src:`${r}/${s[0]}`}]},{tag:"h3",class:"edu-art-subtitle",contents:n,property:"alternativeHeadline"},{tag:"div",class:"edu-art-description",contents:i(t),property:"description"},1<s.length&&{tag:"div",class:"edu-art-carousel",contents:[new o({images:s.map(e=>`${r}/${e}`)}).render()]},0<a.length&&{tag:"div",class:"article-details edu-art-details",contents:[{tag:"h2",contents:"Details"},{tag:"ul",class:"details-list",contents:a.map(e=>({tag:"li",class:"detail",contents:[{tag:"label",contents:e.label},{tag:"div",class:"detail-value",contents:e.value}]}))}]}]}}}},{"../../../../constants":2,"../../../generic-components/image-carousel":5,"../../../lib/article-utils":6}],10:[function(e,t,n){"use strict";const{loadArticles:s}=e("../../../lib/article-utils"),a=e("ks-cheap-translator"),r=a.trad.bind(a),o=e("./edu-article");t.exports=class{constructor(e){this.props=e,this.state={articles:[],loaded:!1},this.id="edu-articles-section",this.loadArticles()}loadArticles(){s("education",a.locale).then(e=>{this.state.articles=e}).catch(e=>console.log(e)).finally(()=>{this.state.loaded=!0,this.refresh()})}renderPlaceholder(){return{tag:"article",class:"placeholder",contents:[{tag:"div"},{tag:"div"},{tag:"div"},{tag:"div"}]}}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}render(){const{articles:e,loaded:t}=this.state;return{tag:"section",class:"edu-articles page-contents-center",id:this.id,contents:t&&0<e.length?e.map(e=>new o({...e}).render()):t&&0===e.length?[{tag:"p",contents:r("Rien de prévu pour le moment")}]:[this.renderPlaceholder()]}}}},{"../../../lib/article-utils":6,"./edu-article":9,"ks-cheap-translator":3}],11:[function(e,t,n){"use strict";const{images_url:s}=e("../../../constants");var a=e("../../lib/web-page");const r=e("ks-cheap-translator"),o=e("./components/edu-articles"),i=r.trad.bind(r),c=[{title:"Programmation",description:"edu-learn-coding",image:"learning_theme_coding.png"},{title:"Dessin numérique et animation 2D",description:"edu-learn-2d",image:"learning_theme_2d.png"},{title:"Maths et physique",description:"edu-learn-math",image:"learning_theme_math.png"},{title:"Aide informatique générale",description:"edu-learn-computer",image:"learning_theme_pc.png"},{title:"Stage GNU/Linux",description:"edu-learn-gnu",image:"learning_theme_linux.png"},{title:"Créer un jeu avec Mentalo",description:"edu-learn-mentalo",image:"learning_theme_mentalo.png"}];class l extends a{render(){return{tag:"div",id:"education-page",typeof:"EducationalOrganization",contents:[{tag:"div",class:"page-header logo-left",contents:[{tag:"div",class:"page-contents-center grid-wrapper",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"image brain",src:`${s}/brain.svg`}]},{tag:"h1",contents:i("Pédagogie")},{tag:"p",contents:i("edu-page-intro")}]}]},{tag:"div",class:"title-banner"},{tag:"section",contents:[{tag:"div",class:"page-contents-center",contents:[{tag:"ul",class:"edu-themes",contents:c.map(e=>({tag:"li",class:"edu-theme",contents:[{tag:"img",width:250,height:140,class:"pixelated",src:`${s}/${e.image}`},{tag:"h3",contents:i(e.title)},{tag:"p",contents:i(e.description)}]}))}]}]},{tag:"h2",class:"edu-section-title page-contents-center",contents:i("Programme XXXX",{date:"2022"})},(new o).render()]}}}t.exports=l},{"../../../constants":2,"../../lib/web-page":8,"./components/edu-articles":10,"ks-cheap-translator":3}],12:[function(e,t,n){"use strict";const s=e("../../run-page");e=e("./education");s(e)},{"../../run-page":13,"./education":11}],13:[function(e,t,n){"use strict";const s=e("object-to-html-renderer"),a=e("./template/template");t.exports=function(e){e=new a({page:new e});s.register("obj2htm"),obj2htm.setRenderCycleRoot(e),obj2htm.renderCycle()}},{"./template/template":15,"object-to-html-renderer":4}],14:[function(e,t,n){"use strict";const{images_url:s}=e("../../../constants"),o=e("ks-cheap-translator"),i=o.trad.bind(o),a=[{url:"/games/",text:"Jeux"},{url:"/education/",text:"Pédagogie"},{url:"/software-development/",text:"Software"}];t.exports=class{constructor(){this.initEventHandlers()}handleBurgerClick(){document.getElementById("nav-menu-list").classList.toggle("responsive-show")}initEventHandlers(){window.addEventListener("click",e=>{"nav-menu-list"===e.target.id||e.target.classList.contains("burger")||e.target.parentNode.classList.contains("burger")||document.getElementById("nav-menu-list").classList.remove("responsive-show")})}handle_chang_lang(e){o.update_translations(e).then(()=>{obj2htm.renderCycle()}).catch(e=>console.log(e))}renderHome(){return{tag:"div",class:"home",contents:[{tag:"a",href:"/",contents:[{tag:"img",alt:"Logo Kuadrado",src:`${s}/logo_kuadrado.svg`},{tag:"img",alt:"Kuadrado Software",class:"logo-text",src:`${s}/logo_kuadrado_txt.svg`}]}]}}renderMenu(e,a=!1,r=""){return{tag:"ul",id:"nav-menu-list",class:a?"submenu":"",contents:e.map(e=>{var{url:t,text:n,submenu:e}=e;const s=`${r}${t}`;return{tag:"li",class:a||window.location.pathname!==s?"":"active",contents:[{tag:"a",href:s,contents:i(n)}].concat(e?[this.renderMenu(e,!0,t)]:[])}}).concat({tag:"li",class:"lang-flags",contents:["fr","en"].map(e=>({tag:"img",src:`${s}/flag-${e}.svg`,class:o.locale===e?"selected":"",onclick:this.handle_chang_lang.bind(this,e)}))})}}renderResponsiveBurger(){return{tag:"div",class:"burger",onclick:this.handleBurgerClick.bind(this),contents:[{tag:"span",contents:"···"}]}}render(){return{tag:"nav",contents:[this.renderHome(),this.renderResponsiveBurger(),this.renderMenu(a)]}}}},{"../../../constants":2,"ks-cheap-translator":3}],15:[function(e,t,n){"use strict";const{in_construction:s}=e("../../config"),{images_url:a}=e("../../constants"),r=e("./components/navbar"),o=e("ks-cheap-translator"),i=o.trad.bind(o);t.exports=class{constructor(e){this.props=e}render(){return{tag:"main",contents:[{tag:"header",contents:[(new r).render()]},s&&{tag:"section",class:"warning-banner",contents:[{tag:"strong",class:"page-contents-center",contents:i("Site en construction ...")}]},{tag:"section",id:"page-container",contents:[this.props.page.render()]},{tag:"footer",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"logo Kuadrado",src:`${a}/logo_kuadrado.svg`},{tag:"img",class:"text-logo",alt:"Kuadrado Software",src:`${a}/logo_kuadrado_txt.svg`}]},{tag:"span",contents:"32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France"},{tag:"div",contents:[{tag:"strong",contents:"<blue>Contact : </blue>"},{tag:"a",href:"mailto:contact@kuadrado-software.fr",contents:"contact@kuadrado-software.fr"}]},{tag:"div",class:"social",contents:[{tag:"strong",contents:`<blue>${i("Sur les réseaux")} : </blue>`},{tag:"a",href:"https://www.linkedin.com/company/kuadrado-software",target:"_blank",contents:"in",title:"Linkedin"},{tag:"a",href:"https://mastodon.gamedev.place/@kuadrado_software",target:"_blank",contents:"m",title:"Mastodon"}]},{tag:"span",contents:`Copyleft 🄯 ${(new Date).getFullYear()} Kuadrado Software | + ${i("kuadrado-footer-copyleft")}`},{tag:"div",contents:[{tag:"span",contents:i("Ce site web est")+" "},{tag:"a",target:"_blank",style_rules:{fontWeight:"bold"},href:"https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md",contents:"OPEN SOURCE"}]}]}]}}}},{"../../config":1,"../../constants":2,"./components/navbar":14,"ks-cheap-translator":3}]},{},[12]); \ No newline at end of file diff --git a/public/games/games.js b/public/games/games.js index 45e5397167b4728f7a5098298dd06a86c175224f..d497e273510df8e18cd000ce74e5620e72c3f2b3 100644 --- a/public/games/games.js +++ b/public/games/games.js @@ -1,4503 +1,2 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ -module.exports = { - images_url: "/assets/images" -} -},{}],2:[function(require,module,exports){ -module.exports = { - build: { - protected_dirs: ["assets", "style", "views", "standard"], - default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"], - }, -}; - -},{}],3:[function(require,module,exports){ -module.exports = { - images_url: `/assets/images`, - data_url: `/assets/data`, - translations_url: "/assets/translations" -}; - -},{}],4:[function(require,module,exports){ -"use strict"; -/** - * A tiny library to handle text translation. - */ - -/** - * init parameter object type - * @typedef TranslatorParam - * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes - * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" - * - * @property {Boolean} use_url_locale_fragment - DEFAULT: true - - * If true, the 1st fragment of the window.location url will be parsed as a language code - * Example: - * window.location - * > https://example.com/en/some-page... - * then the /en/ part of the url will be taken in priority and locale will be set to "en". - * window.location - * > https://example.com/some-page... - * No locale fragment will be found so locale will not be set from url fragment. - * window.location - * > https://example.com/some-page/en - * Doesn't work, locale fragment must be the first framgment - * - * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" - * The key used to saved the current locale into local storage - * - * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations - * Translations files are expected to be named with their corresponding locale code. - * Example: - * if supported_languages is set to ["en", "fr", "it"] - * and translations_url is set to "https://example.com/translations/"" - * Then the expected translations files are - * https://example.com/translations/en.json - * https://example.com/translations/fr.json - * https://example.com/translations/it.json - * The json resources must simple key value maps, value being the translated text. - */ - -module.exports = { - locale: "en", // ISO 639 lowercase language code - supported_languages: ["en"], - translations: {}, - translations_url: "", - use_url_locale_fragment: true, - local_storage_key: "translator-prefered-language", - - /** - * Initialize the lib with params - * @param {TranslatorParam} params - * @returns {Promise} - */ - init(params) { - Object.entries(params).forEach(k_v => { - const [key, value] = k_v; - if ([ - "supported_languages", - "use_url_locale_fragment", - "local_storage_key", - "translations_url" - ].includes(key)) { - this[key] = value; - } - }); - - this.translations_url = this.format_translations_url(this.translations_url); - this.supported_languages = this.format_supported_languages(this.supported_languages); - - return new Promise((resolve, reject) => { - const loc = - (() => {// Locale from url priority 1 - if (this.use_url_locale_fragment) { - const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - return fragment_is_locale ? first_url_fragment : ""; - } else { - return ""; - } - })() - || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 - || (() => { // locale from navigator priority 3 - const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); - return this.supported_languages.includes(navigator_locale) - ? navigator_locale - : this.supported_languages[0] // Default if navigator locale is not supported - })(); - - fetch(`${this.translations_url}${loc}.json`) - .then(response => response.json()) - .then(response => { - this.locale = loc; - this.translations = response; - resolve(); - }) - .catch(err => { - this.locale = "en"; - reject(err); - }); - }); - }, - - /** - * Return a lowercase string without dash. - * If given locale is en-EN, then "en" will be returned. - * @param {String} locale - * @returns A lowercase string - */ - format_locale(locale) { - return locale.split("-")[0].toLowerCase(); - }, - - /** - * Appends a slash at the end of the given string if missing, and returns the url. - * @param {String} url - * @returns {String} - */ - format_translations_url(url) { - if (url.charAt(url.length - 1) !== "/") { - url += "/" - } - return url; - }, - - /** - * Return the array of language codes formatted as lowsercase language code. - * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. - * @param {String[]} languages_codes - * @returns {String[]} - */ - format_supported_languages(languages_codes) { - return languages_codes.map(lc => this.format_locale(lc)); - }, - - /** - * Fetches a new set of translations in case the wanted language changes - * @param {String} locale A lowercase language code - * @returns {Promise} - */ - update_translations(locale) { - locale = this.format_locale(locale); - return new Promise((resolve, reject) => { - fetch(`${this.translations_url}${locale}.json`) - .then(response => response.json()) - .then(response => { - this.translations = response; - this.locale = locale; - - localStorage.setItem(this.local_storage_key, locale); - - const split_path = window.location.pathname.substring(1).split("/"); - const first_url_fragment = split_path[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - - if (fragment_is_locale) { - split_path.splice(0, 1, locale); - const updated_path = split_path.join("/"); - window.history.replaceState(null, "", "/" + updated_path); - } - resolve(); - }) - .catch(err => { - reject(err); - }); - }); - }, - - /** - * Tries to get the translation of the source string, or return the string as it. - * @param {String} text The source text to translate - * @param {Object} params Some dynamic element to insert in the text - * Params can be used if the translated text provide placeholder like {%some_word%} - * Example: - * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) - * -> translation for "Some trad key": "A translated text with {%some_word%}" - * -> will be return as : "A translated text with a dynamic parameter" - * @returns {String} - */ - trad: function (text, params = {}) { - text = this.translations[text] || text; - - Object.keys(params).forEach(k => { - text = text.replace(`{%${k}%}`, params[k]); - }); - - return text; - } -}; -},{}],5:[function(require,module,exports){ -"use strict"; - -const MtlAnimation = require("./model/animation"); -const MentaloEngine = require("./mentalo-engine"); -const MtlScene = require("./model/scene"); -const MtlGame = require("./model/game"); -const MtlSoundTrack = require("./model/sound-track"); -const SCENE_TYPES = require("./model/scene-types"); -const MtlChoice = require("./model/choice"); -const MtlGameObject = require("./model/game-object"); -const FrameRateController = require("./lib/frame-rate-controller"); -const font_tools = require("./lib/font-tools"); -const color_tools = require("./lib/color-tools"); -const shape_tools = require("./lib/shape-tools"); -module.exports = { - MentaloEngine, - MtlAnimation, - MtlScene, - MtlGame, - MtlSoundTrack, - SCENE_TYPES, - MtlChoice, - MtlGameObject, - FrameRateController, - font_tools, - color_tools, - shape_tools, -}; -},{"./lib/color-tools":6,"./lib/font-tools":7,"./lib/frame-rate-controller":8,"./lib/shape-tools":9,"./mentalo-engine":11,"./model/animation":12,"./model/choice":13,"./model/game":15,"./model/game-object":14,"./model/scene":19,"./model/scene-types":18,"./model/sound-track":20}],6:[function(require,module,exports){ -"use strict"; -/** - * Helpers to work with color values - */ - -/** - * Get a RGB color value in String from either "rgb(x,x,x)" or #xxxxxx - * @param {String} color A RGB color value as String in the form: #ffffff or rgb(x,x,x) - * @returns {Uint8[]} An array of 3 values from 0 to 256 for [RED, GREEN, BLUE] - */ -function color_str_to_rgb_array(color) { - color = color.toLowerCase(0); - if (color.includes("rgb")) { - return color.replaceAll(/[a-z\(\)w]/g, "") - .split(",") - .slice(0, 3) - .map(n => parseInt(n)); - } else { - return color.replace("#", "") - .match(/.{0,2}/g) - .slice(0, 3) - .map(hex => parseInt(hex, 16)); - } -} - -/** - * @param {String} color A color value (#xxxxxx or rbg(x,x,x)) - * @returns The average between each tone of the given color. - */ -function get_average_rgb_color_tone(color) { - return color_str_to_rgb_array(color).reduce((tot, n) => tot + n / 3, 0); -} - -/** - * Gets an array of 3 8bits integers for red green and blue and and return a color value as an hexadecimal string #xxxxxx - * @param {Uint8[]} rgb - * @returns {String} The hexacdecimal rgb value of the color including the hash character: #xxxxxx - */ -function rgb_array_to_hex(rgb) { - return `#${rgb.slice(0, 3).map(n => { - const hex = n.toString(16); - return hex.length < 2 ? '0' + hex : hex; - }).join('')}`; -} - -/** - * Gets an array of 4 8bits integers for red green blue and transparency channel and return a color value as an hexadecimal string #xxxxxx - * @param {Uint8[]} rgb - * @returns {String} The hexacdecimal rgba value of the color including the hash character: #xxxxxxxx - */ -function rgba_array_to_hex(rgba) { - rgba = rgba.length === 4 ? rgba : rgba.concat([255]) - return `#${rgba.slice(0, 4).map(n => { - const hex = n.toString(16); - return hex.length < 2 ? '0' + hex : hex; - }).join('')}`; -} - -/** - * Gets a background color as argument and return a calculated optimal foreground color. - * For example if the background is dark the function will return a lighten version of the background. - * @param {String} base_color A RGB color value in hexadecimal form: #ffffff - * @returns - */ -function get_optimal_visible_foreground_color(base_color) { - // create a 2D 1px canvas context - const tmp_canvas = document.createElement("canvas"); - tmp_canvas.width = 1; - tmp_canvas.height = 1; - const ctx = tmp_canvas.getContext("2d"); - - // fill it with the given color - ctx.fillStyle = base_color; - ctx.fillRect(0, 0, 1, 1); - - // Get either a semi-transparent black or semi-transparent white regarding the base color is above or below an avg of 127 - const superpo_color = get_average_rgb_color_tone(base_color) > 127 ? "#0000003f" : "#ffffff3f"; - - // Fill the pixel with the semi-transparent black or white on top of the base color - ctx.fillStyle = superpo_color; - ctx.fillRect(0, 0, 1, 1); - - // Return the flatten result of the superposition - return rgb_array_to_hex(Array.from(ctx.getImageData(0, 0, 1, 1).data)); -} - -/** - * @param {Uint8[]} col1 A RGBA color value as an array of 4 0 to 256 integers - * @param {Uint8[]} col2 A RGBA color value as an array of 4 0 to 256 integers - * @returns {Boolean} true if the 2 colors are equals - */ -function same_rgba(col1, col2) { - return col1[0] === col2[0] - && col1[1] === col2[1] - && col1[2] === col2[2] - && col1[3] === col2[3]; -} - -module.exports = { - get_average_rgb_color_tone, - get_optimal_visible_foreground_color, - color_str_to_rgb_array, - rgb_array_to_hex, - rgba_array_to_hex, - same_rgba, -}; -},{}],7:[function(require,module,exports){ -"use strict"; -/** - * Helpers to works with default web fonts - */ - - -const FONT_FAMILIES = [ - { - category: "Sans serif", values: [ - { value: "sans-serif", text: "sans-serif" }, - { text: "Arial", value: "Arial, sans-serif" }, - { text: "Helvetica", value: "Helvetica, sans-serif" }, - { text: "Verdana", value: "Verdana, sans-serif" }, - { text: "Trebuchet MS", value: "Trebuchet MS, sans-serif" }, - { text: "Noto Sans", value: "Noto Sans, sans-serif" }, - { text: "Gill Sans", value: "Gill Sans, sans-serif" }, - { text: "Avantgarde", value: "Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif" }, - { text: "Optima", value: "Optima, sans-serif" }, - { text: "Arial Narrow", value: "Arial Narrow, sans-serif" } - ], - }, - { - category: "Serif", values: [ - { text: "serif", value: "serif" }, - { text: "Times", value: "Times, Times New Roman, serif" }, - { text: "Didot", value: "Didot, serif" }, - { text: "Georgia", value: "Georgia, serif" }, - { text: "Palatino", value: "Palatino, URW Palladio L, serif" }, - { text: "Bookman", value: "Bookman, URW Bookman L, serif" }, - { text: "New Century Schoolbook", value: "New Century Schoolbook, TeX Gyre Schola, serif" }, - { text: "American Typewriter", value: "American Typewriter, serif" } - ], - }, - { - category: "Monospace", values: [ - { text: "monospace", value: "monospace" }, - { text: "Andale Mono", value: "Andale Mono, monospace" }, - { text: "Courrier New", value: "Courier New, monospace" }, - { text: "Courrier", value: "Courier, monospace" }, - { text: "FreeMono", value: "FreeMono, monospace" }, - { text: "OCR A Std", value: "OCR A Std, monospace" }, - { text: "DejaVu Sans Mono", value: "DejaVu Sans Mono, monospace" }, - ], - }, - { - category: "Cursive", values: [ - { text: "cursive", value: "cursive" }, - { text: "Comic Sans MS", value: "Comic Sans MS, Comic Sans, cursive" }, - { text: "Apple Chancery", value: "Apple Chancery, cursive" }, - { text: "Bradley Hand", value: "Bradley Hand, cursive" }, - { text: "Brush Script MT", value: "Brush Script MT, Brush Script Std, cursive" }, - { text: "Snell Roundhand", value: "Snell Roundhand, cursive" }, - { text: "URW Chancery L", value: "URW Chancery L, cursive" }, - ], - } -]; - -const TEXT_ALIGN_OPTIONS = [ - { value: "left", text: "Left" }, // TODO trad or icon - { value: "right", text: "Right" }, - { value: "center", text: "Center" }, -]; - -const FONT_STYLE_OPTIONS = [ - { value: "normal", text: "Normal" }, - { value: "italic", text: "Italic" }, // TODO trad or icon -]; - -const FONT_WEIGHT_OPTIONS = [ - { value: "1", text: "Thin" }, - { value: "normal", text: "Normal" }, - { value: "900", text: "Bold" }, -]; - -/** - * Gets an object description of a font and returns it formatted as a string that can be given to a 2D drawing context. - * Example : ctx.font = get_canvas_font({font_family:"monospace", font_size: 32}) - * @param {Object} font_data An object description of the font settings, with font_size, font_family, font_style, font_weight.s - * @returns {String} - */ -function get_canvas_font(font_data = {}) { - const { font_size = 20, font_family = "sans-serif", font_style = "normal", font_weight = "normal" } = font_data; - const font_variant = "normal"; - return `${font_style} ${font_variant} ${font_weight} ${font_size.toFixed()}px ${font_family}`; -} - -/** - * @returns {Object} A map of all defined constants for font families, text align options, font styles and font weights. - */ -function get_font_options() { - return { - font_families: FONT_FAMILIES, - text_align_options: TEXT_ALIGN_OPTIONS, - font_style_options: FONT_STYLE_OPTIONS, - font_weight_options: FONT_WEIGHT_OPTIONS, - }; -} - -/** - * An approximation of the average width and height of a character for a 2D drawing context with a given font configuration. - * @param {CanvasRenderingContext2D} ctx - * @returns {Object} An object with {width, height, text_line_height} entries - */ -function get_canvas_char_size(ctx, font_settings) { - ctx.save(); - ctx.font = get_canvas_font(font_settings); - - const str = `Lorem ipsum dolor Sit amet, Consectetur adipiscing elit`; - const width = ctx.measureText(str).width / str.length; - - // Scale width by an approximative 1.1 factor to calculate the height, - // for example for letters like j q p etc that have a part bellow the text base line - const height = ctx.measureText("M").width * 1.1; - const text_line_height = height + height / 4; - - ctx.restore(); - - return { - width, - height, - text_line_height, - interline_height: height / 4 - } -} - -module.exports = { - get_canvas_font, - get_font_options, - get_canvas_char_size, - FONT_FAMILIES, - TEXT_ALIGN_OPTIONS, - FONT_STYLE_OPTIONS, - FONT_WEIGHT_OPTIONS -}; -},{}],8:[function(require,module,exports){ -"use strict"; - -/** - * A little helper class to control the rendering frame rate - */ -class FrameRateController { - /** - * @param {Integer} fps The wanted frame rate in frame per second - */ - constructor(fps) { - this.tframe = performance.now(); - this.interval = 1000 / fps; // convert in milliseconds - this.initial = true; - } - - /** - * @returns {Boolean} true if the defined interval is elapsed. - */ - nextFrameReady() { - if (this.initial) { - this.initial = false; - return true; - } - - const now = performance.now(); - const elapsed = now - this.tframe; - const ready = elapsed > this.interval; - - if (ready) { - this.tframe = now - (elapsed % this.interval); - } - - return ready; - } -} - -module.exports = FrameRateController; -},{}],9:[function(require,module,exports){ -"use strict"; - -/** - * Draws a rectangle on a canvas 2D context with support of corner rounding and border options. - * @param {CanvasRenderingContext2D} ctx - * @param {Integer} x - * @param {Integer} y - * @param {Integer} width - * @param {Integer} height - * @param {Object} options - */ -function draw_rect(ctx, x, y, width, height, options = { - rounded_corners_radius: 0, - border: { width: 0, color: "rgba(0,0,0,0)" }, - fill_color: "black", -}) { - const { rounded_corners_radius = 0, border = { width: 0, color: "rgba(0,0,0,0)" }, fill_color = "black", fill_image } = options; - const smallest_axis = Math.min(width, height); - const radius = rounded_corners_radius > smallest_axis / 2 ? smallest_axis / 2 : rounded_corners_radius; - - ctx.save(); - - ctx.beginPath(); - ctx.arc(x + radius, y + radius, radius, Math.PI, 3 * Math.PI / 2); - ctx.lineTo(x + width - radius, y); - ctx.arc(x + width - radius, y + radius, radius, 3 * Math.PI / 2, 0); - ctx.lineTo(x + width, y + height - radius); - ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2); - ctx.lineTo(x + radius, y + height); - ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI); - ctx.closePath(); - - ctx.clip(); - - if (fill_image) { - ctx.drawImage( - fill_image.src, - 0, 0, - fill_image.src.width, fill_image.src.height, - fill_image.dx, fill_image.dy, - fill_image.dw, fill_image.dh, - ) - } else { - ctx.fillStyle = fill_color; - ctx.fillRect(x, y, width, height); - } - - if (border.width > 0) { - ctx.strokeStyle = border.color; - ctx.lineWidth = border.width; - ctx.stroke(); - } - - ctx.restore(); -} - -module.exports = { - draw_rect, -} -},{}],10:[function(require,module,exports){ -"use strict"; - -const { get_canvas_font, get_canvas_char_size } = require("./font-tools"); - -/** - * A utility function to display text inside of a box coordinates - * @param {CanvasRenderingContext2D} ctx - * @param {String[]} lines - * @param {Object} bounds A bounding box object like {left, right, top, bottom, width, height} - * @param {Object} settings An object with styling settings like {padding, font_color, background_color, ...} - * @param {Object} a_state_ref Any object having a persistent life in the calling instance - * @param {Boolean} streamed Wether the text should be displayed as a progressive text streaming or all at once - * @returns {Boolean} true if the text is fully displayed - */ -function draw_text_in_bounds(ctx, lines, bounds, settings, a_state_ref, streamed) { - ctx.save(); - const char_size = get_canvas_char_size(ctx, settings); - const line_height = char_size.text_line_height; - ctx.font = get_canvas_font(settings); - ctx.fillStyle = settings.font_color; - ctx.textAlign = settings.text_align; - ctx.textBaseline = "top"; - - const get_text_position = () => { - const { padding = 0 } = settings; - const { left, top, width } = bounds; - switch (ctx.textAlign) { - case "left": - return { - x: left + padding, - y: top + padding, - } - case "right": - return { - x: left + width - padding, - y: top + padding, - } - case "center": - return { - x: left + width / 2, - y: top + padding, - } - default: - return { - x: left + padding, - y: top + padding, - } - } - }; - - const text_pos = get_text_position(); - - a_state_ref.stream = a_state_ref.stream || { - line_chars: 0, - line_index: 0, - complete: false, - }; - - const output_lines = streamed ? lines.map((line, i) => { - const stream_state = a_state_ref.stream; - if (i < stream_state.line_index || stream_state.complete) { - return line; - } else if (i > stream_state.line_index) { - return ""; - } else { - const slice = line.slice(0, ++stream_state.line_chars); - if (slice.length === line.length) { - stream_state.complete = lines.length - 1 === stream_state.line_index; - stream_state.line_index = stream_state.complete - ? stream_state.line_index - : stream_state.line_index + 1; - stream_state.line_chars = 0; - } - return slice; - } - }) : (() => { - a_state_ref.stream.complete = true; - return lines; - })(); - - output_lines.forEach(line => { - ctx.fillText(line, text_pos.x, text_pos.y); - text_pos.y += line_height; - }); - - ctx.restore(); - - return a_state_ref.stream.complete; -} - -module.exports = { - draw_text_in_bounds, -} -},{"./font-tools":7}],11:[function(require,module,exports){ -"use strict"; - -const MtlGame = require("./model/game"); -const SCENE_TYPES = require("./model/scene-types"); -const MtlRender = require("./render/render"); -const { supported_locales, get_translated } = require("./translation"); - -/** - * The encapsulating class of all the components of the Mentalo game engine. - * This is the class that should be used from outside to run a game. - */ -class MentaloEngine { - /** - * Initialize A MtlGame instance, populates it with the gama_data given as param. - * Initializes the DOM element to contain the rendering of the engine. - * Defines the listeners for Escape key and initialize a MtlRender instance. - * @param {Object} params - * Parameter params.game_data is required and should be the result of the exportation format of the Mentalo editor app. - */ - constructor(params) { - this.game = new MtlGame(); - this.game.load_data(params.game_data); - - this.game.on_load_resources(() => this.loading = false); - - this.use_locale = params.use_locale && supported_locales.includes(params.use_locale) - ? params.use_locale - : "en"; - - /** - * Allow to exit game by typing Escape or q - * @param {Event} e - */ - this.escape_listener = e => { - const k = e.key.toLowerCase(); - (k === "escape" || k === "q") && this.quit(); - }; - - window.addEventListener("keydown", this.escape_listener); - - this.on_quit_game = function () { - if (this.used_default_container) { - this.container.remove(); - } - window.removeEventListener("keydown", this.escape_listener); - params.on_quit_game && params.on_quit_game(); - }; - - this.used_default_container = false; - - this.container = params.container || (() => { - const el = document.createElement("div"); - el.style.display = "flex"; - el.style.justifyContent = "center"; - el.style.alignItems = "center"; - el.style.position = "absolute"; - el.style.top = 0; - el.style.bottom = 0; - el.style.left = 0; - el.style.right = 0; - el.style.zIndex = 1000; - el.style.overflow = "hidden"; - document.body.appendChild(el); - this.used_default_container = true - return el; - })(); - - this.render = new MtlRender({ - container: this.container, - fullscreen: params.fullscreen, - frame_rate: params.frame_rate, - get_game_settings: this.get_game_settings.bind(this), - get_game_scenes: this.get_game_scenes.bind(this), - get_scene: this.get_scene.bind(this), - get_scene_index: this.get_scene_index.bind(this), - get_inventory: this.get_inventory.bind(this), - on_game_object_click: this.on_game_object_click.bind(this), - on_drop_inventory_object: this.on_drop_inventory_object.bind(this), - on_choice_click: this.on_choice_click.bind(this), - }); - - this.loading = true; - this.will_quit = false; - } - - /** - * Returns a fragment of the game instance ui_options, which are the styling settings of the game interface. - * Can be the settings for text_boxes, choices_panel, inventory, etc. - * @param {String} key The key of the wanted interface settings. - * @returns {Object} The fragment of this.game.game_ui_options for the given key. - */ - get_game_settings(key) { - return this.game.game_ui_options[key]; - } - - /** - * @returns {MtlScene[]} Returns an array of the scenes of the game. - */ - get_game_scenes() { - return this.game.scenes; - } - - /** - * @returns {MtlScene} The scene that is currently displayed - */ - get_scene() { - return this.game.get_scene(); - } - - /** - * @returns {Integer} The index of the scene that is currently displayed - */ - get_scene_index() { - return this.game.scenes.indexOf(this.get_scene()); - } - - /** - * Returns the game objects currently saved in the inventory. - * @returns {MtlGameObject[]} - */ - get_inventory() { - return this.game.state.inventory; - } - - /** - * Sets the flag this.will_quit to true. - * This flag will be parsed in the rendering loop and the engine will then effectively exit the game calling __exit(). - */ - quit() { - this.will_quit = true; - } - - /** - * Clean the render instance, stops the rendering loop and calls the on_quit_game callback. - * This is called when the rendering loop reads the this.will_quit flag as true. - */ - __quit() { - window.cancelAnimationFrame(window.mentalo_engine_animation_id); - this.render.exit_fullscreen(); - this.render.clear_event_listeners(); - this.game.get_soundtrack().stop(); - this.on_quit_game(); - } - - - /** - * Initialize the rendering loop and the MtlRender instance. - * Also checks that the first loaded scene has an image to load. - * If it doesn't the game will exit immediately in order to avoid having a black screen. - * ( If the first scene doesn't have an image to load, the game.on_load_resource callback will - * never be called and the engine will remain in loading state. ) - */ - init() { - window.requestAnimationFrame = window.requestAnimationFrame - || window.mozRequestAnimationFrame - || window.webkitRequestAnimationFrame - || window.msRequestAnimationFrame; - - window.cancelAnimationFrame = window.cancelAnimationFrame - || window.mozCancelAnimationFrame; - - if (window.mentalo_engine_animation_id) { - window.cancelAnimationFrame(window.mentalo_engine_animation_id); - } - - this.render.init(); - - if (this.game.scenes.find(scene => scene.animation.empty)) { - alert(get_translated("Some scenes have an empty image, game cannot be loaded", this.use_locale)); - this.quit(); - } - } - - /** - * Clear the event listeners and rendering element related to the current scene from the MtlRender instance, - * And sets the game current scene state to a new index. - * @param {Integer} index - */ - go_to_scene(index) { - this.render.reset_user_error_popup(); - this.render.clear_event_listeners(); - this.game.get_soundtrack().stop(); - this.game.go_to_scene(index); - this.render.clear_children(); - this.render.reset_text_box_visibility(); - } - - /** - * Event listener for doubleclick on a game object. - * Adds the object to inventory. - * @param {MtlGameObject} obj - */ - on_game_object_click(obj) { - this.game.inventory_has_empty_slot() && this.game.add_object_to_inventory(obj); - } - - /** - * All callback called from the inventory render component when an object image is clicked in the inventory. - * Removes the object from inventory. - * @param {MtlGameObject} obj - */ - on_drop_inventory_object(obj) { - this.game.remove_object_from_inventory(obj); - } - - /** - * Callback called from the choices_panel render component. - * Handles the click on a choice. - * @param {MtlChoice} choice - * @returns - */ - on_choice_click(choice) { - const { destination_scene_index, use_objects } = choice; - if (destination_scene_index === -1) { // -1 is default index when the choice destination scene is not set. - this.render.set_user_error_popup({ - text: get_translated("Destination scene has not been set.", this.use_locale), - }); - return; - } - - if (destination_scene_index === -2) { // -2 is the index used to indicate that destination scene is just the end of program. - this.quit() - return; - } - - // Handle the case where the clicked choice requires some objects to be presents in the inventory. - if (use_objects.value) { - const inventory = this.get_inventory(); - const objs = []; - for (const it of use_objects.items) { - const ob = Array.from(inventory).find(o => o.name === it.name); - if (ob) { - objs.push(ob); - } else { - this.render.set_user_error_popup({ - text: use_objects.missing_object_message, stream_text: true, - }); - return; - } - } - - objs.forEach(o => use_objects.items - .find(it => o.name === it.name) - .consume_object && this.game.consume_game_object(o)); - } - - this.go_to_scene(destination_scene_index); - } - - /** - * Callback called when a scene of type "Cinematic" reaches end. - */ - on_cinematic_end() { - const scene = this.game.get_scene(); - if (scene.end_cinematic_options.quit) { - this.quit(); - } else if (scene.end_cinematic_options.destination_scene_index === -1) { - this.render.set_user_error_popup({ - text: get_translated("Next scene has not been set.", this.use_locale), - on_close: this.quit.bind(this), - }); - } else { - this.go_to_scene(scene.end_cinematic_options.destination_scene_index); - } - } - - /** - * The rendering loop. - * Handles the different states of the engine (quit, loading or runnning normally) - * @returns The id of the registered window.requestAnimationFrame - */ - run_game() { - if (this.will_quit) { - this.__quit(); - return; - } else if (this.loading) { - this.render.draw_loading(); - } else { - if (this.get_scene()._type === SCENE_TYPES.CINEMATIC) { - this.game.update_cinematic_timeout(); - if (this.game.is_cinematic_ended()) { - this.on_cinematic_end(); - } - } - - const sound_track = this.game.get_soundtrack(); - if (sound_track.loaded && !sound_track.is_playing()) { - sound_track.play(); - } - - this.render.draw_game(); - } - - window.mentalo_engine_animation_id = requestAnimationFrame(this.run_game.bind(this)); - } -} - -module.exports = MentaloEngine; -},{"./model/game":15,"./model/scene-types":18,"./render/render":23,"./translation":35}],12:[function(require,module,exports){ -"use strict"; -const Loadable = require("./loadable"); - -/** - * The data type used for a MtlScene animation - */ -class MtlAnimation extends Loadable { - /** - * Initialize an empty instance of Image HtmlElement, - * and registers the onload callback to update the instance with the loaded image actual dimensions. - */ - constructor() { - super(new Image(), "image", "load"); - this.image.onload = () => this.update_dimensions(); - - this.name = ""; - this.dimensions = { - width: 0, - height: 0, - }; - this.frame_nb = 1; - this.frame = 0; - this.speed = 1; - this.play_once = false; - this.initialized = false; - this.finished = false; - } - - /** - * Copies the dimensions of the loaded image at the root level of the instance. - * Deletes the canvas dimensions precalculations created by the SceneAnimation component - */ - update_dimensions() { - this.dimensions = { - width: this.image.width / this.frame_nb, - height: this.image.height, - }; - - if (this.canvas_precalc) { - delete this.canvas_precalc; - } - } - - /** - * Populates the instance with loaded litteral data. - * @param {Object} data - */ - init(data) { - this.empty = data.src === ""; - this.name = data.name; - this.image.src = data.src; - this.frame_nb = data.frame_nb; - this.frame = 0; - this.speed = data.speed; - this.play_once = data.play_once; - this.initialized = true; - } - - /** - * Increments the framcount argument if the animation next frame is ready to be rendered. - * @param {Integer} framecount - */ - update_frame(framecount) { - this.finished = this.play_once && this.frame === this.frame_nb - 1; - if (this.frame_nb > 1 && framecount % this.speed === 0 && !this.finished) { - this.frame = this.frame + 1 <= this.frame_nb - 1 ? this.frame + 1 : 0; - } - } - - /** - * Reset the frame state. - */ - reset_frame() { - this.finished = false; - this.frame = 0; - } -} - -module.exports = MtlAnimation; -},{"./loadable":17}],13:[function(require,module,exports){ -"use strict"; -/** - * The data type used for the choices of a MtlScene - */ -class MtlChoice { - /** - * Initializes the instance with given data or default empty data. - * @param {Object} data - */ - constructor(data = { - text: "", - destination_scene_index: -1, - use_objects: { - value: false, - items: [], - missing_object_message: "", - } - }) { - this.load_data(data); - } - - /** - * Populates the instance with litteral description data - * @param {Object} data - */ - load_data(data) { - this.text = data.text; - this.destination_scene_index = data.destination_scene_index; - this.use_objects = data.use_objects; - } -} - -module.exports = MtlChoice; -},{}],14:[function(require,module,exports){ -"use strict"; -const Loadable = require("./loadable"); - -/** - * The data type used for the game_objects field of a MtlScene - */ -class MtlGameObject extends Loadable { - /** - * Initializes the instance with an empty image and zero position. - */ - constructor() { - super(new Image(), "image", "load"); - this.name = ""; - this.position = { x: 0, y: 0 }; - this.state = {}; - } - - /** - * Populates the instance with a litteral description data object - * @param {Object} data - */ - load_data(data) { - this.image.src = data.image; - this.name = data.name; - this.position = data.position; - } - - /** - * @returns {Object} A {width, height} object for the dimensions of the image. - */ - get_dimensions() { - return { - width: this.image.width, - height: this.image.height, - } - } - - /** - * Returns the bounding box coordinates of the object image as a {top, right, bottom, left} object. - * @returns {Object} - */ - get_bounds() { - const dims = this.get_dimensions(); - return { - top: this.position.y, - right: this.position.x + dims.width, - bottom: this.position.y + dims.height, - left: this.position.x, - } - } -} - -module.exports = MtlGameObject; -},{"./loadable":17}],15:[function(require,module,exports){ -"use strict"; - -const MtlScene = require("./scene"); -const SCENE_TYPES = require("./scene-types"); - -/** - * The data type to encapsulate a loaded game. - */ -class MtlGame { - /** - * Initialize the instance with default data. - */ - constructor() { - this.name = ""; - this.scenes = [new MtlScene()]; - - // Game interface styling preferences - this.game_ui_options = { - // The overall look of the innterface - general: { - background_color: "#000000", - animation_canvas_dimensions: { - width: 600, - ratio: "4:3", - height: function () { - const split_ratio = this.ratio.split(":").map(n => parseInt(n)); - const factor = split_ratio[1] / split_ratio[0]; - return Number((this.width * factor).toFixed()); - }, - }, - }, - // The text box displayed on top of a scene image if a message is defined - text_boxes: { - background_color: "#222222", - font_size: 20, - font_style: "normal", - font_weight: "normal", - font_family: "Arial, sans-serif", - font_color: "#ffffff", - text_align: "left", - padding: 20, - margin: 20, - border_width: 0, - rounded_corners_radius: 0, - }, - // The panel containing the choices buttons below the scene image. - choices_panel: { - background_color: "#222222", - font_size: 20, - font_family: "sans", - font_color: "#ffffff", - font_style: "normal", - text_align: "left", - font_weight: "normal", - container_padding: 20, - choice_padding: 10, - active_choice_background_color: "rgba(255,255,255,.4)", - active_choice_border_width: 0, - active_choice_rounded_corners_radius: 0, - }, - // the inventory grid drawn at the right of the scene image - inventory: { - background_color: "#222222", - columns: 1, - rows: 4, - gap: 10, - padding: 20, - slot_rounded_corner_radius: 0, - slot_border_width: 2, - }, - }; - - this.starting_scene_index = 0; - - // The state of the running game - this.state = { - scene: this.starting_scene_index, - inventory: new Set(), - cinematic_timeout: { - inc: 0, - last_update_time: -1, - timeout: false, - }, - }; - - // This state will be incremented for each when it finishes to load its image and sound resources. - // The loaded_scenes value must be equal to the scenes number for the game to consider that all resources are loaded. - this.load_state = { - loaded_scenes: 0, - }; - } - - /** - * Returns the scene that is currently displayed - * @returns {MtlScene} - */ - get_scene() { - return this.scenes[this.state.scene]; - } - - /** - * @returns {MtlSoundTrack} the soundtrack that is set for the current scene - */ - get_soundtrack() { - return this.get_scene().sound_track; - } - - /** - * Updates the state with a new scene index. - * @param {Integer} index The index of the wanted scene - */ - go_to_scene(index) { - const current_scene = this.get_scene(); - current_scene.animation.reset_frame(); - if (current_scene._type == SCENE_TYPES.CINEMATIC) { - this.state.cinematic_timeout = { - inc: 0, - last_update_time: -1, - timeout: false, - }; - } - this.state.scene = index; - } - - /** - * This is called if the current scene is of type Cinematic. - * UIncrements the cinematic_timeout counter and update the - * cinematic_timeout.timeout state to true if the cinematic - * has been running as much or more time than what is defined in scene.cinematic_duration. - */ - update_cinematic_timeout() { - if (this.state.cinematic_timeout.timeout) return; - - const t = new Date().getTime(); - - if (this.state.cinematic_timeout.last_update_time === -1) { - this.state.cinematic_timeout.last_update_time = t; - } - - this.state.cinematic_timeout.inc = t - this.state.cinematic_timeout.last_update_time; - this.state.cinematic_timeout.timeout = this.get_scene().cinematic_duration * 1000 <= this.state.cinematic_timeout.inc; - } - - /** - * Returns a state wether the running cinematic is ended or not - * @returns {Boolean} - */ - is_cinematic_ended() { - const scene = this.get_scene(); - if (scene.cinematic_duration === 0) return scene.animation.finished; - return this.state.cinematic_timeout.timeout; - } - - /** - * Takes a game object as parameter and references it in the inventory state. - * @param {MtlGameObject} obj - */ - add_object_to_inventory(obj) { - this.state.inventory.add(obj); - } - - /** - * @returns {Boolean} true if inventory has an empty slot. - */ - inventory_has_empty_slot() { - const { columns, rows } = this.game_ui_options.inventory; - return this.state.inventory.size < columns * rows; - } - - /** - * Take a game object as argument and removes its reference from the inventory state. - * @param {MtlGameObject} obj - */ - remove_object_from_inventory(obj) { - this.state.inventory.delete(obj); - } - - /** - * Takes a game object as argument, removes the reference from inventory 7 - * and delete the object from the scene it's defined into. - * @param {MtlGameObject} obj - */ - consume_game_object(obj) { - this.remove_object_from_inventory(obj); - for (const s of this.scenes) { - const found_obj = s.game_objects.find(o => o === obj); - if (found_obj) { - s.game_objects.splice(s.game_objects.indexOf(found_obj), 1); - break; - } - } - } - - /** - * Returns true if the number of scenes having finished to load their loadable resources is equal to the total number of scenes. - * @returns {Boolean} - */ - all_resources_loaded() { - return this.load_state.loadable_elements === this.load_state.loaded_elements; - } - - /** - * Returns a concatenation of all game objects of all scenes. - * @returns {MtlGameObject[]} - */ - get_game_objects() { - return this.scenes.reduce((acc, scene) => acc.concat(scene.game_objects), []); - } - - /** - * Populates the instance with a litteral game desriptor. - * The expected data format is identical to what's exported from the Mentalo app editor, - * or what's returned from the Mentalo API database. - * @param {Object} data - */ - load_data(data) { - this.name = data.name; - - this.starting_scene_index = data.starting_scene_index || 0; - this.state.scene = this.starting_scene_index; - - this.scenes = data.scenes.map(scene => { - const scene_instance = new MtlScene(); - scene_instance.load_data(scene); - scene_instance.on_load_group_complete(() => { - this.load_state.loaded_scenes++; - if (this.load_state.loaded_scenes === this.scenes.length) { - this.on_load_resources_callback && this.on_load_resources_callback(); - } - }) - return scene_instance; - }); - - this.game_ui_options = Object.assign(data.game_ui_options, { - general: { - background_color: data.game_ui_options.general.background_color, - animation_canvas_dimensions: Object.assign(data.game_ui_options.general.animation_canvas_dimensions, { - height: function () { - const split_ratio = this.ratio.split(":").map(n => parseInt(n)); - const factor = split_ratio[1] / split_ratio[0]; - return Number((this.width * factor).toFixed()); - }, - }), - } - }); - } - - /** - * Sets the callback to call when all resources of all scenes are fully loaded. - * @param {Function} callback - */ - on_load_resources(callback) { - this.on_load_resources_callback = callback; - } -} - -module.exports = MtlGame; -},{"./scene":19,"./scene-types":18}],16:[function(require,module,exports){ -"use strict"; - -/** - * A structure to manage a group of object that extends from the ./Loadable class. - * For each handled loadable, increments a loadable_elements state, and once the loadable has finished - * to load whatever it needs to load, the loaded_elements state is incremented. - */ -class LoadableGroup { - constructor() { - this.loadable_elements = 0; - this.loaded_elements = 0; - } - - /** - * Attaches a on_load callback to the Loadable object given as argument. - * Increments the loadable_elements state. - * @param {Loadable} object Any object that extends the Loadable class - */ - add_loadable(object) { - this.loadable_elements++; - /** - * The callback that will be call by the Loadable object when it will have finished to load what it needs to load. - */ - object.on_load(() => { - this.loaded_elements++; - if (this.loadable_elements === this.loaded_elements && this.on_load_group_complete_custom_callback) { - this.on_load_group_complete_custom_callback(); - } - }); - } - - /** - * Defines a custom callback to call when all handled loadable objects have finished loading. - * @param {Function} callback - */ - on_load_group_complete(callback) { - this.on_load_group_complete_custom_callback = callback; - } -} - -module.exports = LoadableGroup; -},{}],17:[function(require,module,exports){ -"use strict"; - -/** - * An abstract type that must be inherited by any object that have a resource to load. - * Can be an image or a sound. - */ -class Loadable { - /** - * Initialization of the actual loadable element. - * @param {HTMLElement} loadable_element Image() or Audio() for example - * @param {String} field_name the name of the field that references the loadable element in the instance. - * @param {String} event_name the name of the event that is dispatched by the loadable - * html element when it has loaded a resource ("load" or "loadeddata") - */ - constructor(loadable_element, field_name, event_name) { - this.loaded = false; - this.init_loadable_element(loadable_element, field_name, event_name); - } - - /** - * Initializes event listeners for the registered loadable_element. - * @param {HTMLElement} loadable_element - * @param {String} field_name the name of the field that references the element - * @param {String} event_name the event to use for resource loading (images uses onload, audio uses onloadeddata ...) - */ - init_loadable_element(loadable_element, field_name, event_name) { - this[field_name] = loadable_element; - - this.load_listener = () => { - this.loaded = true; - this.on_load_callback(); - this.clear(); - }; - - this[field_name].addEventListener(event_name, this.load_listener); - - this.clear = () => { - this[field_name].removeEventListener(event_name, this.load_listener); - } - } - - /** - * Sets a callback to call when the loadable_element has finished to load its resource. - * @param {Function} callback - */ - on_load(callback) { - this.on_load_custom_callback = callback; - } - - /** - * The callback that's called when the loadable element as finished loading its resource - */ - on_load_callback() { - this.loaded = true; - this.on_load_custom_callback && this.on_load_custom_callback(); - } -} - -module.exports = Loadable; -},{}],18:[function(require,module,exports){ -/** - * An enum like object to describe the 2 possible type that can have a MtlScene - */ - -module.exports = { - PLAYABLE: "Playable", - CINEMATIC: "Cinematic", -}; -},{}],19:[function(require,module,exports){ -"use strict"; - -const SCENE_TYPES = require("./scene-types"); -const MtlAnimation = require("./animation"); -const MtlSoundTrack = require("./sound-track"); -const MtlChoice = require("./choice"); -const MtlGameObject = require("./game-object"); -const LoadableGroup = require("./loadable-group"); - -/** - * The type used for the scenes of a MtlGame. - * Extends the LoadableGroup class because it manages multiple objects of the class Loadable. - */ -class MtlScene extends LoadableGroup { - /** - * Initializes the instance with default empty data. - */ - constructor() { - super(); - const default_data = { - name: "", - _type: SCENE_TYPES.PLAYABLE, - animation: new MtlAnimation(), - sound_track: new MtlSoundTrack(), - choices: [], - text_box: "", - game_objects: [], - cinematic_duration: 0, - end_cinematic_options: { - destination_scene_index: -1, - quit: false, - } - }; - - this.name = default_data.name; - this._type = default_data._type; - this.animation = default_data.animation; - this.sound_track = default_data.sound_track; - - this.choices = default_data.choices; - this.text_box = default_data.text_box; - this.game_objects = default_data.game_objects; - - this.cinematic_duration = default_data.cinematic_duration || 0; - this.end_cinematic_options = default_data.end_cinematic_options; - } - - /** - * Populates the instance from a litteral descriptor. - * Creates and populates the instances of Animation, Soundtrack and GameObjects - * @param {Object} data - */ - load_data(data) { - this.name = data.name; - this._type = data._type; - - const animation = new MtlAnimation(); - animation.init(data.animation); - this.animation.clear(); - this.animation = animation; - this.animation.image.src && this.add_loadable(this.animation); - - const sound_track = new MtlSoundTrack(); - sound_track.init(data.sound_track); - this.sound_track.clear(); - this.sound_track = sound_track; - this.sound_track.src && this.add_loadable(this.sound_track); - - this.choices = data.choices.map(c => new MtlChoice(c)); - this.text_box = data.text_box; - this.cinematic_duration = data.cinematic_duration || 0; - - this.game_objects = data.game_objects.map(gob => { - const inst = new MtlGameObject(); - inst.load_data(gob); - this.add_loadable(inst); - return inst; - }); - - this.end_cinematic_options = data.end_cinematic_options; - } -} - -module.exports = MtlScene; -},{"./animation":12,"./choice":13,"./game-object":14,"./loadable-group":16,"./scene-types":18,"./sound-track":20}],20:[function(require,module,exports){ -"use strict"; - -const Loadable = require("./loadable"); - -/** - * The data type used to represent and manage the soundtrack of a scene - */ -class MtlSoundTrack extends Loadable { - constructor() { - super(new Audio(), "audio", "loadeddata"); - this.name = ""; - this.initialized = false; - } - - /** - * Reset the instance to empty values. - */ - reset() { - this.init_loadable_element(new Audio(), "audio", "loadeddata"); - this.name = ""; - this.initialized = false; - } - - /** - * Populates the instance from a litteral descriptor - * @param {Object} data - */ - init(data) { - this.empty = data.src === ""; - this.audio.src = data.src; - this.name = data.name; - this.audio.loop = data._loop; - this.initialized = true; - } - - /** - * Plays the audio resource. - */ - play() { - this.audio.play(); - } - - /** - * Stops the audio player. - */ - stop() { - this.audio.pause(); - this.audio.currentTime = 0; - } - - /** - * Returns the state of the inner audio element - * @returns {Boolean} - */ - is_playing() { - return !this.audio.paused - } -} - -module.exports = MtlSoundTrack; -},{"./loadable":17}],21:[function(require,module,exports){ -"use strict"; - -const ChoicesPanelMetrics = require("./choices-panel-metrics"); - -class CanvasDimensions { - constructor(params) { - this.params = params; - - // The original dimensions of the image part of the canvas - this.image = { width: 0, height: 0 }; - - // The scaled dimensions of the rendering canvas - this.width = 0; - this.height = 0; - - // Scaled dimensions of the inventory zone - this.inventory = { width: 0, height: 0 }; - - // Scaled dimensions of the choices zone - this.choices_panel = { width: 0, height: 0 }; - - // The scale factor that will have been calculated - this.scale_factor = 1; - - this.init_dimensions(); - } - - /** - * Calculate the dimensions from game settings - */ - init_dimensions() { - const { get_game_settings } = this.params; - - const general_settings = get_game_settings("general"); - - const image_h = general_settings.animation_canvas_dimensions.height(); - const image_w = general_settings.animation_canvas_dimensions.width; - const inventory_w = this.get_inventory_base_width(); - const base_w = image_w + inventory_w; - - this.choices_panel_metrics = this.get_choices_panel_metrics(); - - const max_playground_width = window.screen.width - 200; - const max_playground_height = window.screen.height - 200; - - const base_h = image_h + this.choices_panel_metrics.get_total_height(); - const playground_ratio = base_w / base_h; - - const screen_ratio = window.screen.width / window.screen.height; - - // Calculate the width of the canvas to the maximum possible scale - if (screen_ratio > playground_ratio) { - // Screen is more panoramic than game canvas so we scale canvas to the maximum height - const scaled_w = max_playground_height * playground_ratio - this.width = scaled_w <= max_playground_width ? scaled_w : max_playground_width; - } else { - // Image is more panoramic so its scaled to the maximum width - const scaled_h = max_playground_width * (base_h / base_w) - this.height = scaled_h <= max_playground_height ? scaled_h : max_playground_height; - this.width = this.height * playground_ratio; - } - - this.scale_factor = this.width / base_w; - - this.choices_panel_metrics = this.choices_panel_metrics.to_scaled(this.scale_factor); - - this.image = { - width: image_w * this.scale_factor, - height: image_h * this.scale_factor, - }; - - this.inventory = { - width: inventory_w * this.scale_factor, - height: this.image.height, - }; - - this.choices_panel = { - width: this.width, - height: this.choices_panel_metrics.get_total_height(), - }; - - this.height = this.image.height + this.choices_panel.height; - } - - /** - * The width of the inventory panel - * @returns {Integer} - */ - get_inventory_base_width() { - const { get_game_settings } = this.params; - const image_h = get_game_settings("general").animation_canvas_dimensions.height(); - const inventory_style = get_game_settings("inventory"); - const gap = inventory_style.gap; - const h = image_h - (2 * inventory_style.padding); - const gap_h = (inventory_style.rows - 1) * gap; - const gap_w = (inventory_style.columns - 1) * gap - const slot_side = (h - gap_h) / inventory_style.rows; - return (inventory_style.columns * slot_side) - + (2 * inventory_style.padding) - + gap_w; - } - - /** - * The metrics in pixels for each row of choices in the choices panel - * @returns {ChoicesPanelMetrics} - */ - get_choices_panel_metrics() { - const { get_game_settings, get_game_scenes } = this.params; - const choices_panel_settings = get_game_settings("choices_panel"); - const general_settings = get_game_settings("general"); - - const container_padding = choices_panel_settings.container_padding; - const container_width = general_settings.animation_canvas_dimensions.width - + this.get_inventory_base_width() - - (2 * container_padding); - - const scenes_choices = get_game_scenes().map(s => { return { choices: s.choices } }); - - return new ChoicesPanelMetrics({ - settings: choices_panel_settings, - container_width, - scenes_choices, - }); - } -} - -module.exports = CanvasDimensions; -},{"./choices-panel-metrics":22}],22:[function(require,module,exports){ -"use strict"; - -const { get_canvas_char_size } = require("../lib/font-tools"); - -/** - * A class to facilitate the handling of the chocies panel outer and inner dimensions - */ -class ChoicesPanelMetrics { - constructor(params) { - this.params = params; - const { scale_factor = 1 } = this.params; - this.container_width = this.params.container_width * scale_factor; - this.settings = this.get_scaled_settings(scale_factor); - this.scenes_formatted_choices = this.get_scenes_formatted_choices(); - this.container_padding_height = 2 * this.settings.container_padding; - this.rows = this.get_rows_metrics(); - } - - /** - * @returns {object} the choices panel settings object given in the instance parameters - * scaled by the scale_factor parameter - */ - get_scaled_settings(scale_factor) { - const { settings } = this.params; - return Object.assign({ ...settings }, { - font_size: settings.font_size * scale_factor, - active_choice_border_width: settings.active_choice_border_width * scale_factor, - active_choice_rounded_corners_radius: settings.active_choice_rounded_corners_radius * scale_factor, - choice_padding: settings.choice_padding * scale_factor, - container_padding: settings.container_padding * scale_factor, - }); - } - - /** - * @param {Number} row_nb Optional - 1 or 2 if we don't want to get the number of choice rows at its max - * @param {Object[]} scene_choices Optional - The choices that we want to parse, default is all choices. - * @returns {Object[]} An array of 2 objects like {text_height, padding_height} for each choices row - */ - get_rows_metrics(row_nb, scene_choices) { // params will be default if undefined - const { settings } = this; - const max_lines_per_row = this.get_choices_max_lines_per_row(row_nb, scene_choices); - - const char_size = get_canvas_char_size(window.mentalo_drawing_context, settings); - const text_line_h = char_size.text_line_height; - const interline_h = char_size.interline_height; - - const choice_padding_height = 2 * settings.choice_padding; - - const height_text_row_1 = max_lines_per_row[0] * text_line_h - interline_h; - const height_text_row_2 = max_lines_per_row[1] > 0 // If some scene have more than 2 choices, there will be displayed on 2 rows. - ? max_lines_per_row[1] * text_line_h - interline_h - : 0; - - return [ - { - text_height: height_text_row_1, - padding_height: choice_padding_height - }, - { - text_height: height_text_row_2, - padding_height: height_text_row_2 > 0 ? choice_padding_height : 0 - } - ]; - } - - /** - * The maximum number of text lines that each row of choice can have - * @returns {Integer[]} An array with 2 items. 1st is for the upper choices row, second is for the lower one. - */ - get_choices_max_lines_per_row(row_nb, scenes_choices) { - const choices_max_row_nb = row_nb || this.get_max_choices_row_per_scene(); - const formatted_choices = scenes_choices || this.scenes_formatted_choices; - - // get the largest number of text lines per choice row - const max_lines_per_row = [0, 0]; - Array.from({ length: choices_max_row_nb }).forEach((_row, i) => { - const slice_index = i === 0 ? [0, 2] : [2, 4]; - const largest = formatted_choices - .map(s_choices => Math.max(...s_choices - .slice(...slice_index) - .map(c => c.text_lines.length) - )) - .reduce((res, nb) => Math.max(res, nb), 0); - max_lines_per_row[i] = largest; - }); - - return max_lines_per_row; - } - - /** - * @returns {Integer} The maximum number of rows that the choices_panel can have in the loaded game. - */ - get_max_choices_row_per_scene(scenes_slice_index) { - const scenes_choices = scenes_slice_index - ? this.params.scenes_choices.slice(scenes_slice_index, scenes_slice_index + 1) - : this.params.scenes_choices; - - let choices_max_row_nb = 1; - if (Math.max(...scenes_choices.map(s => s.choices.length)) > 2) { - choices_max_row_nb = 2 - } - - return choices_max_row_nb; - } - - /** - * Parses the raw text of each scene choices and returns them with an additional - * text_lines field with the text split into lines ready to be rendered in canvas. - * @typedef FormattedChoice - * @property {String[]} text_lines - * @property {...MtlChoice} - the other MtlChoice fields - * - * @returns {FormattedChoice[]} - */ - get_scenes_formatted_choices() { - const { scenes_choices } = this.params; - const { settings, container_width } = this; - - const char_size = get_canvas_char_size(window.mentalo_drawing_context, settings); - - const { choice_padding } = settings; - const error_offset = .9; - const choice_max_width = ((container_width / 2) - (2 * choice_padding)) * error_offset; - const max_chars_per_row = choice_max_width / char_size.width; - - return scenes_choices.map(s => s.choices.map(c => { - const words = c.text.split(" "); - const lines = [""]; - let line_i = 0; - - words.forEach(w => { - if ((lines[line_i] + w).length >= max_chars_per_row) { - line_i++; - lines.push(""); - } - lines[line_i] = `${lines[line_i]}${lines[line_i] === "" ? "" : " "}${w}`; - }); - return Object.assign({ ...c }, { text_lines: lines }); - })); - } - - /** - * The total calculated height of the choices panel - * @returns {Number} - */ - get_total_height(rows) { - rows = rows || this.rows; - return rows.reduce( - (total, row) => total + row.text_height + row.padding_height, 0 - ) + this.container_padding_height; - } - - /** - * A copy of this instance with values scaled by the given factor - * @param {Number} scale_factor the scaling factor - * @returns {ChoicesPanelMetrics} - */ - to_scaled(scale_factor) { - return new ChoicesPanelMetrics({ ...this.params, scale_factor }); - } - - /** - * The choices panel bounding zone has first been calculated to the maximum, - * but certain scenes may need less space to display their choices. - * This return the only necessary height for a given scene - * @param {MtlChoice[]} scene_choices - * @returns {Number} A bounding box {top, left, right, bottom, width, height} - */ - get_minimum_panel_height_for_scene(scene_index) { - const scene_choices = this.scenes_formatted_choices.slice(scene_index, scene_index + 1); - const choices_row_nb = scene_choices[0].length > 2 ? 2 : 1; - const rows = this.get_rows_metrics(choices_row_nb, scene_choices); - return this.get_total_height(rows); - } - - /** - * Get the metrics for a given row of choices - * @param {Integer} scene_index - * @param {Number} scale_fator - */ - get_one_choices_row_metrics(scene_index, row_index) { - const scene_choices = this.scenes_formatted_choices.slice(scene_index, scene_index + 1); - const rows = this.get_rows_metrics(row_index + 1, scene_choices); - const row = rows[row_index]; - return Object.assign(row, { - text_height: row.text_height, - padding_height: row.padding_height, - }); - } -} - -module.exports = ChoicesPanelMetrics; -},{"../lib/font-tools":7}],23:[function(require,module,exports){ -"use strict"; - -const FrameRateController = require("../lib/frame-rate-controller"); -const { get_optimal_visible_foreground_color } = require("../lib/color-tools"); -const { get_canvas_char_size } = require("../lib/font-tools"); -const SCENE_TYPES = require("../model/scene-types"); -const ChoiceCpt = require("./ui-components/choice-cpt"); -const ChoicesPanelCpt = require("./ui-components/choices-panel-cpt"); -const ClosingIconCpt = require("./ui-components/closing-icon-cpt"); -const GameObjectCpt = require("./ui-components/game-object-cpt"); -const InventoryCpt = require("./ui-components/inventory-cpt"); -const InventoryObjectCpt = require("./ui-components/inventory-object-cpt"); -const InventorySlotCpt = require("./ui-components/inventory-slot-cpt"); -const SceneAnimationCpt = require("./ui-components/scene-animation-cpt"); -const TextBoxCpt = require("./ui-components/text-box-cpt"); -const UserErrorPopup = require("./ui-components/user-error-popup"); -const CanvasDimensions = require("./canvas-dimensions"); - -/** - * The class that handles all interactions with the canvas 2D drawing context to draw the game. - */ -class MtlRender { - constructor(params) { - this.params = params; - const frame_rate = this.params.frame_rate || 30; - this.fps_controller = new FrameRateController(frame_rate); - - this.params.container.style.backgroundColor = this.params.get_game_settings("general").background_color; - this.canvas = document.createElement("canvas"); - this.canvas.style.backgroundColor = "black"; - - window.mentalo_drawing_context = this.canvas.getContext("2d"); - - this.canvas_dimensions = {}; - this.canvas_zones = {}; - this.event_listeners = { - game_objects: [], - inventory: [], - text_box: [], - }; - - this.state = { - user_error_popup: { is_set: false, text: "", on_close: function () { } } - }; - - this.loading_frame = 0; - } - - /** - * Initializes the rendering of the game, creates the components, the canvas zones and requests full screen. - */ - init() { - if (this.params.fullscreen) { - this.set_full_screen(); - } - - this.set_canvas_dimensions(); - this.create_canvas_zones(); - - this.params.container.appendChild(this.canvas); - - const ctx = window.mentalo_drawing_context; - // Obsolete properties for browsers compatibility - ctx.mozImageSmoothingEnabled = false; - ctx.webkitImageSmoothingEnabled = false; - ctx.msImageSmoothingEnabled = false; - ctx.imageSmoothingEnabled = false; - - this.create_components(); - } - - /** - * Removes the event listeners attached to the components. - */ - clear_event_listeners() { - Object.values(this.components).forEach(cpt => { - cpt.clear_event_listeners(); - }); - } - - /** - * Removes the elements rendered as children of the rendering components - * (Example a TextBoxCpt is a child the scene_animation component) - * @param {Object} options can defined a set of constructor names to exclude from clean up - */ - clear_children(options = { exclude: [] }) { - Object.values(this.components).forEach(cpt => { - cpt.clear_children(options); - }); - } - - /** - * Reset the scene text box visibility state to true. - * The text box visibility state is controlled by the scene_animation component and not directly by the text box component. - */ - reset_text_box_visibility() { - this.components.scene_animation.set_text_box_visibility(true) - } - - /** - * Tries to enable the window fullscreen mode. - */ - set_full_screen() { - const body = document.body; - body.requestFullScreen = body.requestFullScreen - || body.webkitRequestFullScreen - || body.msRequestFullscreen - || body.mozRequestFullScreen; - try { - body.requestFullScreen(); - } catch (err) { - console.error(err.message) - } - } - - /** - * Exits the window fullscreen mode - */ - exit_fullscreen() { - if (document.fullscreenElement) { - document.exitFullscreen(); - } - } - - /** - * Updates the error popup state with new values given as argument. - * @param {Object} params Must have a text<String> entry and a on_close<Function> entry. - */ - set_user_error_popup(params) { - this.state.user_error_popup = { - is_set: true, - text: params.text, - stream_text: !!params.stream_text, - on_close: function () { - params.on_close && params.on_close(); - }, - }; - this.clear_event_listeners(); - // Clears children excluding everything but errorpopup - this.clear_children({ exclude: ["TextBoxCpt", "InventoryCpt", "ChoicesPanelCpt", "GameObjectCpt"] }); - } - - /** - * Unsets error popup - */ - clear_user_error_popup() { - this.reset_user_error_popup(); - this.clear_event_listeners(); - this.clear_children({ exclude: ["TextBoxCpt", "InventoryCpt", "ChoicesPanelCpt", "GameObjectCpt"] }); - } - - /** - * Sets error popup to empty values - */ - reset_user_error_popup() { - this.state.user_error_popup = { - is_set: false, - text: "", - on_close: function () { } - }; - } - - /** - * Callback called when error popup is closed - */ - on_close_user_error_popup() { - this.state.user_error_popup.on_close(); - this.clear_user_error_popup(); - } - - /** - * Parses game data and calculates optimal dimensions for the canvas. - */ - set_canvas_dimensions() { - const { get_game_settings, get_game_scenes } = this.params; - - this.canvas_dimensions = new CanvasDimensions({ - get_game_settings, - get_game_scenes, - }); - - this.canvas.width = this.canvas_dimensions.width; - this.canvas.height = this.canvas_dimensions.height; - } - - /** - * @returns {Object} The choices_panel settings scaled to display canvas size - */ - get_scaled_choices_panel_settings() { - return this.canvas_dimensions.choices_panel_metrics.settings; - } - - /** - * @returns {Object} The inventory settings scaled to display canvas size - */ - get_scaled_inventory_settings() { - const { scale_factor } = this.canvas_dimensions; - const { get_game_settings } = this.params; - - const base_settings = get_game_settings("inventory"); - - return Object.assign({ ...base_settings }, { - slot_rounded_corner_radius: base_settings.slot_rounded_corner_radius * scale_factor, - slot_border_width: base_settings.slot_border_width * scale_factor, - gap: base_settings.gap * scale_factor, - padding: base_settings.padding * scale_factor, - }); - } - - /** - * @returns {Object} The text boxes settings scaled to display canvas size - */ - get_scaled_text_boxes_settings() { - const { scale_factor } = this.canvas_dimensions; - const { get_game_settings } = this.params; - const text_box_settings = get_game_settings("text_boxes"); - - return Object.assign({ ...text_box_settings }, { - font_size: text_box_settings.font_size * scale_factor, - padding: text_box_settings.padding * scale_factor, - margin: text_box_settings.margin * scale_factor, - rounded_corners_radius: text_box_settings.rounded_corners_radius * scale_factor, - border_width: text_box_settings.border_width * scale_factor, - }) - } - - /** - * Precalculates the bounding boxes for each canvas zone. - * set_canvas_dimensions() must have been called before this. - */ - create_canvas_zones() { - const metrics = this.canvas_dimensions; - const inventory_settings = this.get_scaled_inventory_settings(); - const choices_panel_settings = this.get_scaled_choices_panel_settings(); - - this.canvas_zones = { - root: { - left: 0, - top: 0, - right: metrics.width, - bottom: metrics.height, - width: metrics.width, - height: metrics.height, - padding: 0, - }, - scene_animation: { - left: 0, - top: 0, - right: metrics.image.width, - bottom: metrics.image.height, - width: metrics.image.width, - height: metrics.image.height, - clear_color: "black", - padding: 0, - }, - inventory: { - left: metrics.image.width, - top: 0, - right: metrics.width, - bottom: metrics.inventory.height, - width: metrics.width - metrics.image.width, - height: metrics.inventory.height, - clear_color: inventory_settings.background_color, - padding: inventory_settings.padding, - }, - choices_panel: { - left: 0, - top: metrics.image.height, - right: metrics.width, - bottom: metrics.height, - width: metrics.width, - height: metrics.height - metrics.image.height, - clear_color: choices_panel_settings.background_color, - padding: choices_panel_settings.container_padding - }, - }; - } - - /** - * @returns {Boolean} true if scene is of type Playable - */ - scene_is_not_cinematic() { - return this.params.get_scene()._type === SCENE_TYPES.PLAYABLE; - } - - /** - * Creates the tree of all the components to render. All component extends the UiComponent class. - * The root component instances (Scene animation, Inventory, Choices panel) will only be created once, - * but children components are functions of their parents so they are recreated when get_children is called on a parent component. - * For example A TextBoxCpt is a children a SceneAnimationCpt, so the text box component will be recreated each time - * scene_animation.get_children() is called. This allow to display those components as functions of dynamical states. - */ - create_components() { - const { get_game_settings, get_inventory, get_scene, get_game_scenes, get_scene_index } = this.params; - const { scale_factor } = this.canvas_dimensions; - const scene_is_not_cinematic = this.scene_is_not_cinematic.bind(this); - this.components = { - scene_animation: new SceneAnimationCpt({ - bounding_zone: this.canvas_zones.scene_animation, - get_animation: () => get_scene().animation, - next_frame_ready: () => this.fps_controller.nextFrameReady(), - get_children: () => { - const scene = get_scene(); - return scene.game_objects.map(o => { - const parent_zone = this.canvas_zones.scene_animation; - const obj_pos = { - x: (o.position.x * scale_factor) + parent_zone.left, - y: (o.position.y * scale_factor) + parent_zone.top, - }; - - const obj_dim = { - w: o.image.width * scale_factor, - h: o.image.height * scale_factor, - }; - - const obj_bounds = { - left: obj_pos.x, - right: obj_pos.x + obj_dim.w, - top: obj_pos.y, - bottom: obj_pos.y + obj_dim.h, - width: obj_dim.w, - height: obj_dim.h, - }; - - const game_objects_cpt_params = { - bounding_zone: obj_bounds, - position: obj_pos, - dimensions: obj_dim, - image: o.image, - is_in_inventory: () => get_inventory().has(o), - }; - - const obj_cpt = new GameObjectCpt(game_objects_cpt_params); - - obj_cpt.add_event_listener({ - event_type: "mousemove", - listener: e => { - if (!get_inventory().has(o)) { - const cursor_is_over_obj = e.offsetX >= obj_bounds.left - && e.offsetX <= obj_bounds.right - && e.offsetY >= obj_bounds.top - && e.offsetY <= obj_bounds.bottom; - obj_cpt.state.draw_border = cursor_is_over_obj; - } - } - }); - - obj_cpt.add_event_listener({ - event_type: "click", - listener: e => { - if (!get_inventory().has(o) - && e.offsetX >= obj_bounds.left - && e.offsetX <= obj_bounds.right - && e.offsetY >= obj_bounds.top - && e.offsetY <= obj_bounds.bottom) { - this.params.on_game_object_click(o); - this.clear_event_listeners(); - this.clear_children({ exclude: "TextBoxCpt" }); - } - } - }); - - return obj_cpt; - }) - .concat(scene.text_box ? [ - (() => { - const text_box_settings = this.get_scaled_text_boxes_settings(); - - // calculation of the minimum bound for textbox, without content - const text_box_bounds = (() => { - const parent_zone = this.canvas_zones.scene_animation; - const padding = text_box_settings.padding; - const margin = text_box_settings.margin; - const left = parent_zone.left + margin; - const right = parent_zone.right - margin; - const bottom = parent_zone.bottom - margin; - const top = bottom - (2 * padding); - return { - left, - right, - top, - bottom, - width: right - left, - height: bottom - top, - } - })(); - - const closing_icon_radius = 10 * scale_factor; - const closing_icon_center = { - x: text_box_bounds.right - (closing_icon_radius / 2), - y: text_box_bounds.top + (closing_icon_radius / 2), - }; - - const closing_icon_params = { - color: text_box_settings.font_color, - radius: closing_icon_radius, - center: closing_icon_center, - background_color: text_box_settings.background_color, - line_width: Math.floor(2 * scale_factor), - bounding_zone: { - left: closing_icon_center.x - closing_icon_radius, - right: closing_icon_center.x + closing_icon_radius, - top: closing_icon_center.y - closing_icon_radius, - bottom: closing_icon_center.y + closing_icon_radius, - width: 2 * closing_icon_radius, - height: 2 * closing_icon_radius, - }, - }; - - const text_box = new TextBoxCpt({ - text: get_scene().text_box, - settings: text_box_settings, - bounding_zone: text_box_bounds, - get_visibility_state: () => this.components.scene_animation.state.text_box_visible, - get_children: () => [new ClosingIconCpt(closing_icon_params)], - }); - - const close_text_box_icon = text_box.children[0]; - text_box.children[0].add_event_listener({ - event_type: "click", - listener: e => { - const bounds = close_text_box_icon.params.bounding_zone; - const click_over_icon = e.offsetX >= bounds.left - && e.offsetX <= bounds.right - && e.offsetY >= bounds.top - && e.offsetY <= bounds.bottom; - - const { scene_animation } = this.components; - - if (click_over_icon && scene_animation.state.text_box_visible) { - scene_animation.set_text_box_visibility(false); - } - } - }); - return text_box; - })(), - ] : []) - .concat(this.state.user_error_popup.is_set ? [ - (() => { - const { text, stream_text } = this.state.user_error_popup; - const use_settings = this.get_scaled_text_boxes_settings(); - const parent_bounds = this.canvas_zones.scene_animation - const char_size = get_canvas_char_size(window.mentalo_drawing_context, use_settings); - - const popup_width = parent_bounds.width / 2; - const padding = char_size.width * 2; - const max_chars_per_row = (popup_width - (2 * padding)) / char_size.width; - const text_lines = [""]; - - let line_i = 0; - text.split(" ").forEach(word => { - if (`${text_lines[line_i]}${word} `.length > max_chars_per_row) { - line_i++; - text_lines.push(""); - } - text_lines[line_i] += `${word} `; - }); - - const line_h = char_size.text_line_height; - const popup_height = (text_lines.length * line_h) + (2 * padding) - char_size.interline_height; - - const background_color = use_settings.background_color; - const text_color = use_settings.font_color; - - const popup_bounds = { - left: parent_bounds.left + (parent_bounds.width / 2) - (popup_width / 2), - top: parent_bounds.top + (parent_bounds.height / 2) - (popup_height / 2), - right: parent_bounds.left + (parent_bounds.width / 2) + (popup_width / 2), - bottom: parent_bounds.top + (parent_bounds.height / 2) - (popup_height / 2) + popup_height, - width: popup_width, - height: popup_height, - clear_color: background_color, - }; - - const popup_params = { - settings: use_settings, - bounding_zone: popup_bounds, - modal_bounds: this.canvas_zones.scene_animation, - clear_modal_color: "#0004", - text_lines, - padding, - stream_text, - get_children: () => { - const center = { - x: popup_bounds.right - 5, - y: popup_bounds.top + 5, - }; - - const radius = 15; - - const closing_icon_bounds = { - left: center.x - radius, - right: center.x + radius, - top: center.y - radius, - bottom: center.y + radius, - width: radius * 2, - height: radius * 2, - }; - - const closing_icon_params = { - color: text_color, - radius, - center, - background_color, - line_width: 2, - bounding_zone: closing_icon_bounds, - }; - - const closing_icon = new ClosingIconCpt(closing_icon_params); - - closing_icon.add_event_listener({ - event_type: "click", - listener: e => { - if (e.offsetX >= closing_icon_bounds.left - && e.offsetX <= closing_icon_bounds.right - && e.offsetY >= closing_icon_bounds.top - && e.offsetY <= closing_icon_bounds.bottom) { - this.on_close_user_error_popup(); - } - } - }); - - return [closing_icon]; - } - }; - const popup = new UserErrorPopup(popup_params); - return popup; - })() - ] : []); - }, - }), - inventory: new InventoryCpt({ - bounding_zone: this.canvas_zones.inventory, - get_inventory, - is_visible: scene_is_not_cinematic, - invisible_clear_color: get_game_settings("general").background_color, - get_children: () => { - const settings = this.get_scaled_inventory_settings(); - - const slots_zone = { - left: this.canvas_zones.inventory.left + settings.padding, - top: this.canvas_zones.inventory.top + settings.padding, - right: this.canvas_zones.inventory.right - settings.padding, - bottom: this.canvas_zones.inventory.bottom - settings.padding, - width: this.canvas_zones.inventory.width - (2 * settings.padding), - height: this.canvas_zones.inventory.height - (2 * settings.padding) - }; - - const gap = settings.gap; - const h = slots_zone.height; - const gap_h = (settings.rows - 1) * gap; - const slot_side = (h - gap_h) / settings.rows; - const slots_total_w = (slot_side * settings.columns) + (gap * (settings.columns - 1)); - const center_slot_x_offset = (slots_zone.width - (slots_total_w)) / 2; - - const slots = []; - let slot_i = 0; - Array.from({ length: settings.rows }).forEach((_row, i) => { - const top = slots_zone.top + (i * (slot_side + gap)); - Array.from({ length: settings.columns }).forEach((_col, j) => { - const left = slots_zone.left + (j * (slot_side + gap)) + center_slot_x_offset; - - const slot_bounds = { - left, - top, - right: left + slot_side, - bottom: top + slot_side, - width: slot_side, - height: slot_side - }; - - const stroke_color = get_optimal_visible_foreground_color(settings.background_color); - - const slot_params = { - bounding_zone: slot_bounds, - stroke_color, - is_visible: scene_is_not_cinematic, - settings, - get_children: () => { - const inventory_object = new InventoryObjectCpt({ - slot_index: slot_i, - is_visible: scene_is_not_cinematic, - settings, - stroke_color, - get_game_object: index => { - const obj = Array.from(get_inventory())[index]; - if (obj) { - const obj_dim = obj.get_dimensions(); - const obj_ratio = obj_dim.width / obj_dim.height; - const scaled_dim = { width: 0, height: 0 }; - const pos = { x: 0, y: 0 }; - if (obj_ratio > 1) { - // object image landscape oriented - scaled_dim.width = slot_side; - scaled_dim.height = slot_side * (obj_dim.height / obj_dim.width); - pos.x = slot_bounds.left; - pos.y = slot_bounds.top + ((slot_side / 2) - (scaled_dim.height / 2)); - } else if (obj_ratio < 1) { - // portrait oriented - scaled_dim.height = slot_side; - scaled_dim.width = slot_side * obj_ratio; - pos.y = slot_bounds.top; - pos.x = slot_bounds.left + ((slot_side / 2) - (scaled_dim.width / 2)); - } else { - // square - scaled_dim.width = slot_side; - scaled_dim.height = slot_side; - pos.x = slot_bounds.left; - pos.y = slot_bounds.top; - } - return { - ref: obj, - position: pos, - dimensions: scaled_dim, - }; - } else { - return undefined; - } - }, - bounding_zone: slot_bounds, - get_children: () => [ - new ClosingIconCpt({ - is_visible: scene_is_not_cinematic, - color: "#bf5e43", // Some kind of red - center: { - x: slot_bounds.left + (slot_side / 2), - y: slot_bounds.top + (slot_side / 2) - }, - radius: slot_side / 4, - line_width: slot_side / 20 > 2 ? slot_side / 20 : 2, - bounding_zone: { ...slot_bounds, clear_color: "rgba(0,0,0,0.2)" }, - }), - ] - }); - - inventory_object.add_event_listener({ - event_type: "mousemove", - listener: e => { - const obj = inventory_object.params.get_game_object(inventory_object.params.slot_index); - const bounds = inventory_object.params.bounding_zone; - - inventory_object.state.draw_inventory_close_icon = inventory_object.params.is_visible() - && !!obj - && get_scene().game_objects.includes(obj.ref) - && ( - e.offsetX >= bounds.left - && e.offsetX <= bounds.right - && e.offsetY >= bounds.top - && e.offsetY <= bounds.bottom - ); - } - }); - - inventory_object.children[0].add_event_listener({ - event_type: "click", - listener: e => { - if (!inventory_object.params.is_visible()) return false; - const obj = inventory_object.params.get_game_object(inventory_object.params.slot_index); - const bounds = inventory_object.params.bounding_zone; - const mouse_over_slot = e.offsetX >= bounds.left - && e.offsetX <= bounds.right - && e.offsetY >= bounds.top - && e.offsetY <= bounds.bottom; - if (!!obj && mouse_over_slot && get_scene().game_objects.includes(obj.ref)) { - this.params.on_drop_inventory_object(obj.ref); - this.clear_event_listeners(); - this.clear_children({ exclude: "TextBoxCpt" }); - } - } - }); - - return [inventory_object]; - }, - }; - - const slot = new InventorySlotCpt(slot_params); - slots.push(slot); - slot_i++; - }); - }); - return slots; - }, - }), - choices_panel: new ChoicesPanelCpt({ - bounding_zone: this.canvas_zones.choices_panel, - minimum_bounding_zone: () => { - const initial_zone = this.canvas_zones.choices_panel; - const minium_height = this.canvas_dimensions.choices_panel_metrics - .get_minimum_panel_height_for_scene( - get_scene_index() - ); - const maximum_height = this.canvas_dimensions.choices_panel_metrics - .get_total_height(); - const dif = maximum_height - minium_height; - - return Object.assign({ ...initial_zone }, { - bottom: initial_zone.bottom - dif, - height: initial_zone.height - dif, - }); - }, - is_visible: scene_is_not_cinematic, - invisible_clear_color: get_game_settings("general").background_color, - get_children: () => Array.from({ - length: this.canvas_dimensions.choices_panel_metrics - .get_max_choices_row_per_scene(get_scene_index()) * 2 - } - ).map((_, i) => { - const choices_settings = this.get_scaled_choices_panel_settings(); - const writable_parent_zone = { // The writable zone of the choices panel. = The choices_panel bounding box excluding the padding. - left: this.canvas_zones.choices_panel.left + choices_settings.container_padding, - right: this.canvas_zones.choices_panel.right - choices_settings.container_padding, - top: this.canvas_zones.choices_panel.top + choices_settings.container_padding, - bottom: this.canvas_zones.choices_panel.bottom - choices_settings.container_padding, - width: 0, - height: 0, - }; - - writable_parent_zone.width = writable_parent_zone.right - writable_parent_zone.left; - writable_parent_zone.height = writable_parent_zone.bottom - writable_parent_zone.top; - - const choice_width = writable_parent_zone.width / 2; - - const { choices_panel_metrics } = this.canvas_dimensions; - const choices_height_per_row = choices_panel_metrics.rows.map(row => row.text_height + row.padding_height); - - const choice_bounds = { - left: writable_parent_zone.left + ((i % 2) * choice_width), // if i % 2 choice is on the right side of the panel - top: writable_parent_zone.top + ((i > 1 ? 1 : 0) * choices_height_per_row[0]), // If index is > 1, choice is on the lower row - right: writable_parent_zone.left + ((i % 2) * choice_width) + choice_width, - bottom: writable_parent_zone.top + ((i > 1 ? 1 : 0) * choices_height_per_row[1]) + choices_height_per_row[i > 1 ? 1 : 0], - width: choice_width, - height: choices_height_per_row[i > 1 ? 1 : 0], - }; - - const choice_cpt = new ChoiceCpt({ - is_visible: scene_is_not_cinematic, - minimum_bounding_zone: () => { - if (!choice_cpt.minimum_bounding_zone) { - const row_index = i > 1 ? 1 : 0; - const metric = this.canvas_dimensions.choices_panel_metrics.get_one_choices_row_metrics( - get_scene_index(), row_index, - ); - - const min_height = metric.text_height + metric.padding_height; - - const dif_upper_row = (() => { - const choices_cpt = this.components.choices_panel.children; - const cpt_index = choices_cpt.indexOf(choice_cpt); - return cpt_index >= 2 - ? choice_bounds.top - choices_cpt[0].params.minimum_bounding_zone().bottom - : 0; - })(); - - choice_cpt.minimum_bounding_zone = Object.assign({ ...choice_bounds }, { - height: min_height, - top: choice_bounds.top - dif_upper_row, - bottom: choice_bounds.top - dif_upper_row + min_height, - }); - } - return choice_cpt.minimum_bounding_zone; - }, - get_formatted_choice: () => { - const scene_choices = this.canvas_dimensions.choices_panel_metrics.scenes_formatted_choices[ - get_game_scenes().indexOf(get_scene()) - ]; - return scene_choices.length - 1 >= i ? scene_choices[i] : undefined; - }, - settings: choices_settings, - }); - - choice_cpt.add_event_listener({ - event_type: "mousemove", - listener: e => { - choice_cpt.state.active = - choice_cpt.params.is_visible() - && !!choice_cpt.params.get_formatted_choice() - && choice_cpt.is_hover(e); - } - }); - - choice_cpt.add_event_listener({ - event_type: "click", - listener: () => { - if (choice_cpt.state.active) { - const choice = choice_cpt.params.get_formatted_choice(); - const success = this.params.on_choice_click(choice); - if (success) { - this.clear_event_listeners(); - this.clear_children(); - } - } - } - }); - - return choice_cpt; - }) - }), - }; - } - - /** - * Draw a loading state on the black screen while some game resources are loading. - * This shouldn't show up a lot except if some resources are really big... - */ - draw_loading() { - const ctx = window.mentalo_drawing_context; - ctx.save(); - ctx.font = '25px monospace'; - ctx.fillStyle = "black"; - ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); - ctx.fillStyle = "white"; - ctx.fillText("Loading", 50, window.innerHeight / 2); - const dots = Array.from({ length: ++this.loading_frame }).map(() => '.').join(''); - ctx.font = '8px monospace'; - ctx.fillText(dots, 50, window.innerHeight / 2 + 20); - ctx.restore(); - } - - /** - * Executes the draw method of each registered component. - */ - draw_game() { - Object.values(this.components).forEach(cpt => cpt.draw()); - } -} - -module.exports = MtlRender; -},{"../lib/color-tools":6,"../lib/font-tools":7,"../lib/frame-rate-controller":8,"../model/scene-types":18,"./canvas-dimensions":21,"./ui-components/choice-cpt":24,"./ui-components/choices-panel-cpt":25,"./ui-components/closing-icon-cpt":26,"./ui-components/game-object-cpt":27,"./ui-components/inventory-cpt":28,"./ui-components/inventory-object-cpt":29,"./ui-components/inventory-slot-cpt":30,"./ui-components/scene-animation-cpt":31,"./ui-components/text-box-cpt":32,"./ui-components/user-error-popup":34}],24:[function(require,module,exports){ -"use strict"; - -const { draw_text_in_bounds } = require("../../lib/text-tools"); -const { draw_rect } = require("../../lib/shape-tools"); -const MtlUiComponent = require("./ui-component"); - -/** - * The rendering component for a scene choice. - */ -class ChoiceCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("ChoiceCpt") - } - - /** - * Draw the choice on the registered 2D drawing context - */ - draw() { - super.draw(); - const { - get_formatted_choice, - settings, - } = this.params; - - const bounding_zone = this.params.minimum_bounding_zone(); - - const writable_zone = { - left: bounding_zone.left + settings.choice_padding, - right: bounding_zone.right - settings.choice_padding, - top: bounding_zone.top + settings.choice_padding, - bottom: bounding_zone.bottom - settings.choice_padding, - width: bounding_zone.width - (2 * settings.choice_padding), - height: bounding_zone.height - (2 * settings.choice_padding), - }; - - const choice = get_formatted_choice(); - - const ctx = window.mentalo_drawing_context; - - if (choice) { - if (this.state.active) { - const { left, top, width, height } = bounding_zone; - - draw_rect(ctx, left, top, width, height, { - fill_color: settings.active_choice_background_color, - rounded_corners_radius: settings.active_choice_rounded_corners_radius, - border: { - width: settings.active_choice_border_width, - color: settings.font_color, - } - }); - } - - draw_text_in_bounds(ctx, choice.text_lines, writable_zone, settings, {}, false); - } - } - - /** - * @param {Event} e - * @returns {Boolean} true if the mouse event is inside the choice bounding box - */ - is_hover(e) { - const bounds = this.params.minimum_bounding_zone(); - return e.offsetX >= bounds.left - && e.offsetX <= bounds.right - && e.offsetY >= bounds.top - && e.offsetY <= bounds.bottom; - } -} - -module.exports = ChoiceCpt; -},{"../../lib/shape-tools":9,"../../lib/text-tools":10,"./ui-component":33}],25:[function(require,module,exports){ -"use strict"; - -const MtlUiComponent = require("./ui-component"); - -/** - * The rendering component for the game choices panel - */ -class ChoicesPanelCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("ChoicesPanelCpt") - } - - /** - * Draw the choices panel and the choices as children components on the registered 2D drawing context - */ - draw() { - super.draw(); - if (this.params.is_visible()) { - this.clear_bounding_zone({ clear_color: this.params.invisible_clear_color }); - this.clear_bounding_zone({ use_bounding_zone: this.params.minimum_bounding_zone() }) - this.draw_children(); - } else { - this.clear_bounding_zone({ clear_color: this.params.invisible_clear_color }) - } - } -} - -module.exports = ChoicesPanelCpt; -},{"./ui-component":33}],26:[function(require,module,exports){ -"use strict"; - -const MtlUiComponent = require("./ui-component"); - -/** - * A rendering component that displays a closing icon (a cross inside a circle) - */ -class ClosingIconCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("ClosingIconCpt") - } - - /** - * Uses standard vectorial drawing methods to draw a circle and a cross at - * given coordinates on the registered 2D drawing context. - */ - draw() { - super.draw(); - this.clear_bounding_zone(); - const { color, center, radius = 5, line_width = 2, background_color = "rgba(0,0,0,0)" } = this.params; - const ctx = window.mentalo_drawing_context; - const padding = radius / 1.5; - - const cross_coords = { - left: center.x - radius + padding, - right: center.x + radius - padding, - top: center.y - radius + padding, - bottom: center.y + radius - padding, - }; - - ctx.fillStyle = background_color; - ctx.beginPath(); - ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); - ctx.fill(); - - ctx.strokeStyle = color; - ctx.lineWidth = line_width; - ctx.beginPath(); - ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(cross_coords.left, cross_coords.top); - ctx.lineTo(cross_coords.right, cross_coords.bottom); - ctx.moveTo(cross_coords.left, cross_coords.bottom) - ctx.lineTo(cross_coords.right, cross_coords.top); - ctx.stroke(); - } -} - -module.exports = ClosingIconCpt; -},{"./ui-component":33}],27:[function(require,module,exports){ -"use strict"; - -const MtlUiComponent = require("./ui-component"); - -/** - * A component that draws a GameObject - */ -class GameObjectCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("GameObjectCpt") - } - - /** - * Draws the image of the object on the canvas. - */ - draw() { - super.draw(); - const { position, dimensions, image, is_in_inventory } = this.params; - if (is_in_inventory()) { - return; - } - - const ctx = window.mentalo_drawing_context; - - ctx.drawImage( - image, - 0, 0, - image.width, image.height, - position.x, position.y, - dimensions.w, dimensions.h, - ); - - if (this.state.draw_border) { - ctx.lineWidth = 1; - ctx.strokeStyle = "rgba(180, 180, 180, 0.5)"; - ctx.strokeRect(position.x - 5, position.y - 5, dimensions.w + 10, dimensions.h + 10); - } - } -} - -module.exports = GameObjectCpt; -},{"./ui-component":33}],28:[function(require,module,exports){ -"use strict"; - -const MtlUiComponent = require("./ui-component"); - -/** - * The component that draws the inventory panel - */ -class InventoryCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("InventoryCpt") - } - - /** - * Draws the panel on the canvas, T - * the grid slots and the game object images are draws as children components (InventorySlotCpt and InventoryObjectCpt) - */ - draw() { - super.draw(); - if (this.params.is_visible()) { - this.clear_bounding_zone(); - this.draw_children(); - } else { - this.clear_bounding_zone({ clear_color: this.params.invisible_clear_color }) - } - } -} - -module.exports = InventoryCpt; -},{"./ui-component":33}],29:[function(require,module,exports){ -"use strict"; - -const { draw_rect } = require("../../lib/shape-tools"); -const MtlUiComponent = require("./ui-component"); - -/** - * The component to display a game object image inside a grid slot of the inventory panel. - */ -class InventoryObjectCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("InventoryObjectCpt") - } - - /** - * If a game object exists for the slot bound to this instance, its image will be drawn cropped inside a bounding rectangle. - * When a game object is hovered in the inventory, the draw_inventory_closing_icon is updated and a red cross will be drawn - * on top of the object image. If the cross get's clicked the object is removed from inventory. - */ - draw() { - super.draw(); - const { get_game_object, slot_index, settings, bounding_zone, stroke_color } = this.params; - const game_object = get_game_object(slot_index); - if (game_object) { - const ctx = window.mentalo_drawing_context; - - const image = game_object.ref.image; - draw_rect(ctx, bounding_zone.left, bounding_zone.top, bounding_zone.width, bounding_zone.height, { - fill_image: { - src: image, - dw: game_object.dimensions.width, - dh: game_object.dimensions.height, - dx: game_object.position.x, - dy: game_object.position.y, - }, - rounded_corners_radius: settings.slot_rounded_corner_radius, - border: { - width: settings.slot_border_width, - color: stroke_color, - }, - }); - - if (this.state.draw_inventory_close_icon) { - this.draw_children(); // drop object icon - } - } - } -} - -module.exports = InventoryObjectCpt; -},{"../../lib/shape-tools":9,"./ui-component":33}],30:[function(require,module,exports){ -"use strict"; - -const { draw_rect } = require("../../lib/shape-tools"); -const MtlUiComponent = require("./ui-component"); - -/** - * The component to draw a grid slot in the inventory panel. - */ -class InventorySlotCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("InventorySlotCpt"); - } - - /** - * Draws a rectangle as the grid slot. - * Calls draw_children to draw the image of the game object stored in that slot if there is one. - */ - draw() { - super.draw(); - const { bounding_zone, stroke_color, settings } = this.params; - const { left, top, width, height } = bounding_zone; - const ctx = window.mentalo_drawing_context; - - draw_rect(ctx, left, top, width, height, { - fill_color: "rgba(0,0,0,0)", - rounded_corners_radius: settings.slot_rounded_corner_radius, - border: { - width: settings.slot_border_width, - color: stroke_color, - } - }); - - this.draw_children(); - } -} - -module.exports = InventorySlotCpt; -},{"../../lib/shape-tools":9,"./ui-component":33}],31:[function(require,module,exports){ -"use strict"; - -const MtlUiComponent = require("./ui-component"); - -/** - * The component that draws the scene animation. - */ -class SceneAnimationCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("SceneAnimationCpt"); - this.framecount = 0; - // This state controls the visibility of the text box child component. - this.state.text_box_visible = true; - } - - /** - * Sets the text_box_visibility state. - * @param {Boolean} value - */ - set_text_box_visibility(value) { - this.state.text_box_visible = value; - } - - /** - * Increments the animation frame count - */ - update_framecount() { - this.framecount = this.framecount + 1 <= Number.MAX_SAFE_INTEGER ? this.framecount + 1 : 0; - } - - /** - * Draw the scene animation on the canvas at the frame given by the animation state - * and draw each child component like text box, game objects and error popup. - */ - draw() { - super.draw(); - const { bounding_zone, get_animation, next_frame_ready } = this.params; - if (next_frame_ready()) { - this.clear_bounding_zone(); - const ctx = window.mentalo_drawing_context; - - const animation = get_animation(); - animation.update_frame(this.framecount); - const dim = animation.dimensions; - const w = dim.width; - const h = dim.height; - const offsetX = animation.frame * w; - - const cw = bounding_zone.width; - const ch = bounding_zone.height; - - // center the image - if (!animation.canvas_precalc) { - animation.canvas_precalc = { - dx: bounding_zone.left, - dy: bounding_zone.top, - dw: cw, - dh: ch, - }; - - if (w / h > cw / ch) { - // image is more panoramic than canvas - animation.canvas_precalc.dw = cw; - animation.canvas_precalc.dh = cw * (h / w); - // center vertically - animation.canvas_precalc.dy = bounding_zone.top + ((ch - animation.canvas_precalc.dh) / 2); - } else if (cw / ch > w / h) { - // canvas is more panoramic - animation.canvas_precalc.dh = ch; - animation.canvas_precalc.dw = ch * (w / h); - // center horizontally - animation.canvas_precalc.dx = bounding_zone.left + ((cw - animation.canvas_precalc.dw) / 2); - } else { - // same ratio - animation.canvas_precalc.dh = ch; - animation.canvas_precalc.dw = cw; - } - } - - const { dx, dy, dw, dh } = animation.canvas_precalc; - ctx.drawImage( - animation.image, - offsetX, 0, - w, h, - dx, dy, - dw, dh - ); - - this.draw_children(); - this.update_framecount(); - } - } -} - -module.exports = SceneAnimationCpt; -},{"./ui-component":33}],32:[function(require,module,exports){ -"use strict"; - -const { draw_text_in_bounds } = require("../../lib/text-tools"); -const { get_canvas_char_size } = require("../../lib/font-tools"); -const { draw_rect } = require("../../lib/shape-tools"); -const MtlUiComponent = require("./ui-component"); - -/** - * The component that handles the displaying of a scene text boxe. - */ -class TextBoxCpt extends MtlUiComponent { - constructor(params) { - super(params); - this.set_str_id("TextBoxCpt"); - this.state.text = {}; - } - - /** - * Initialize precalculations for the text box, text dimensions, split lines, etc. - */ - init() { - const { text, settings, bounding_zone } = this.params; - - const char_size = get_canvas_char_size(window.mentalo_drawing_context, settings); - - const line_height = char_size.text_line_height; - const error_offset = 3 * char_size.width; - const chars_per_line = (bounding_zone.width - error_offset) / char_size.width; - const output_lines = []; - const lines = text.split("\n"); - - lines.forEach(line => { - const res = [""]; - let target_i = 0; - for (const word of line.split(" ")) { - if ((res[target_i] + word + " ").length > chars_per_line) { - res.push(""); - target_i++; - } - res[target_i] += word + " "; - } - - res.forEach(l => output_lines.push(l)); - }); - - const text_height = output_lines.length * line_height - char_size.interline_height; - - const updated_bounds = Object.assign({ ...bounding_zone }, { - height: bounding_zone.height + text_height, - top: bounding_zone.top - text_height, - }); - - this.state.text = { - lines: output_lines, - line_height, - bounding_zone: updated_bounds, - }; - - // Update closing icon position - const closing_icon = this.children[0]; - let closing_icon_bounds = closing_icon.params.bounding_zone; - closing_icon_bounds = Object.assign(closing_icon_bounds, { - top: closing_icon_bounds.top - text_height, - bottom: closing_icon_bounds.bottom - text_height, - }); - - closing_icon.params.center.y -= text_height; - - this.state.initialized = true; - } - - /** - * Draw the text box and the text inside of it. - * The text is not drawn at once, the characters are drawn one by one at each frame - * and a local text.stream state is updated until text is complete. - */ - draw() { - if (!this.params.get_visibility_state()) { - return; - } - - super.draw(); - - const { settings } = this.params; - - if (!this.state.initialized) { - this.init(); - } - - const ctx = window.mentalo_drawing_context; - const { lines, bounding_zone } = this.state.text; - - const box_pos = { x: bounding_zone.left, y: bounding_zone.top }; - const box_width = bounding_zone.width; - const box_height = bounding_zone.height; - - draw_rect(ctx, box_pos.x, box_pos.y, box_width, box_height, { - fill_color: settings.background_color, - rounded_corners_radius: settings.rounded_corners_radius, - border: { - width: settings.border_width, - color: settings.font_color, - }, - }); - - const complete = draw_text_in_bounds(ctx, lines, bounding_zone, settings, this.state.text, true); - - if (complete) { // draw closing_icon if text is entirely displayed - this.draw_children(); - } - } -} - -module.exports = TextBoxCpt; -},{"../../lib/font-tools":7,"../../lib/shape-tools":9,"../../lib/text-tools":10,"./ui-component":33}],33:[function(require,module,exports){ -"use strict"; - -const { draw_rect } = require("../../lib/shape-tools"); - -// const { draw_rect } = require("../lib/shape-tools"); - -/** - * A generic class that must be extended by all components registered in MtlRender.components. - * It provides helping methods to handle event listeners and children components. - */ -class MtlUiComponent { - constructor(params) { - this.set_str_id(); - this.params = params; - this.params.event_listeners = this.params.event_listeners || []; - this.params.get_children = this.params.get_children || (() => []); - this.params.is_visible = this.params.is_visible || (() => true); - this.children = []; - this.children_set = false; - this.set_children(); - this.state = { - event_listeners_initialized: false, - }; - - this.init_event_listeners(); - } - - /** - * Provide a identifier for the object. Default is contructor name. - * Should be called from child class to set a custom id. - * @param {String} str - */ - set_str_id(str = this.constructor.name) { - this.str_id = str; - } - - /** - * Draws a black rectangle on all the surface of the bounding zone defined for this component - * @param {Object} options can provide a clear_color<String> rgba value - */ - clear_bounding_zone(options = {}) { - let { bounding_zone } = this.params; - const { use_bounding_zone } = options; - bounding_zone = use_bounding_zone || bounding_zone; - const { left, top, width, height } = bounding_zone; - draw_rect( - window.mentalo_drawing_context, - left, top, width, height, - { - fill_color: options.clear_color || bounding_zone.clear_color || "rgba(0,0,0,0)" - } - ); - } - - /** - * Removes the event listeners attached to this component and to children components. - * @param {Object} options can pass a recursive flag wether children must be cleared or not. Default is true. - */ - clear_event_listeners(options = { recursive: true }) { - this.params.event_listeners.forEach(ref => { - window.removeEventListener(ref.event_type, ref.listener); - }); - - if (options.recursive) { - this.children.forEach(child => { - child.clear_event_listeners(); - }); - } - - this.state.event_listeners_initialized = false; - } - - /** - * Removes children components from this component. - * Options can define some component to exlude identifying them by their str_id. - * The excluded component will be kept as persistent components. - * @param {Object} options - */ - clear_children(options) { - const { exclude } = options; - const persistent_children = []; - - this.children.forEach(child => { - if (exclude.includes(child.str_id)) { - persistent_children.push(child); - } - }); - - this.children = persistent_children; - this.children_set = false; - } - - /** - * Register an array of children components in this component. - * This is called when chlidren_set is false. Which happend at initialization and when clear_children is called. - * Doing this allows saving the get_children() calculation at each frame. - * This children components are concatenated recursively with their own children components. - */ - set_children() { - this.children = this.children.concat( - this.params.get_children() - .filter(child => { - // If this child has been kept as a persistent component, keep it as it. - const keep_previous_child = this.children.find(ch => ch.str_id === child.str_id); - return !keep_previous_child; - }) - ); - - this.children_set = true; - } - - /** - * Attaches a event listener to this component - * @param {Object} obj A descriptor of the event listener like : - * { - * event_type: "click", - * listener: e => { - * ... something to do on click that component - * } - * } - */ - add_event_listener(obj) { - const len = this.params.event_listeners.push(obj); - const ref = this.params.event_listeners[len - 1]; - window.addEventListener(ref.event_type, ref.listener); - } - - /** - * Attaches the event listeners given as parameter to the component - */ - init_event_listeners() { - const { event_listeners } = this.params; - event_listeners.forEach(obj => this.add_event_listener(obj)); - this.state.event_listeners_initialized = true - } - - /** - * Call the draw method of the children component - */ - draw_children() { - if (!this.children_set) { - this.set_children(); - } - this.children.forEach(c => c.draw()); - } - - /** - * Initializes the event listeners if it's not already done. - * This must be called by the inherited call draw method. - */ - draw() { - if (!this.state.event_listeners_initialized) { - this.init_event_listeners(); - } - } -} - -module.exports = MtlUiComponent; -},{"../../lib/shape-tools":9}],34:[function(require,module,exports){ -"use strict"; - -const { draw_text_in_bounds } = require("../../lib/text-tools"); -const { draw_rect } = require("../../lib/shape-tools"); -const MtlUiComponent = require("./ui-component"); - -/** - * A component to display an error message in a popup - */ -class UserErrorPopup extends MtlUiComponent { - constructor(params) { - super(params); - this.state = { - text: {}, - }; - - this.set_str_id("UserErrorPopup"); - } - - /** - * Draw the popup to the canvas. - * Draws the box, then draw the text lines. - * draw_children is for the closing_icon. - */ - draw() { - super.draw(); - const { text_lines, bounding_zone, padding, settings, modal_bounds, clear_modal_color } = this.params; - this.clear_bounding_zone({ use_bounding_zone: modal_bounds, clear_color: clear_modal_color }); - - const ctx = window.mentalo_drawing_context; - const { left, top, width, height } = bounding_zone; - - draw_rect(ctx, left, top, width, height, { - fill_color: settings.background_color, - rounded_corners_radius: settings.rounded_corners_radius, - border: { - width: settings.border_width, - color: settings.font_color, - }, - }); - - const complete = draw_text_in_bounds( - ctx, - text_lines, - bounding_zone, - Object.assign({ ...settings }, padding), - this.state.text, - !!this.params.stream_text - ); - - if (complete) { - this.draw_children(); - } - } -} - -module.exports = UserErrorPopup; -},{"../../lib/shape-tools":9,"../../lib/text-tools":10,"./ui-component":33}],35:[function(require,module,exports){ -const supported_locales = ["en", "fr", "es"]; - -/** - * Translations for the default error messages. - */ - -const translations = { - "Some scenes have an empty image, game cannot be loaded": { - fr: "Certaines scènes ont une image vide, le jeu ne peut pas être chargé", - es: "Algunas escenas tienen una imagen vacÃa, el juego no se puede cargar" - }, - "Destination scene has not been set.": { - fr: "La scène de destination n'a pas été définie.", - es: "La escena de destino no ha sido definida.", - }, - "Next scene has not been set.": { - fr: "La scène suivante n'a pas été définie", - es: "La siguiente escena no ha sido definida.", - }, -}; - -function get_translated(str, locale) { - return translations[str] && locale !== "en" && supported_locales.includes(locale) - ? translations[str][locale] - : str; -} - -module.exports = { - get_translated, - supported_locales, -}; -},{}],36:[function(require,module,exports){ -"use strict"; - -module.exports = { - register_key: "objectToHtmlRender", - - /** - * Register "this" as a window scope accessible variable named by the given key, or default. - * @param {String} key - */ - register(key) { - const register_key = key || this.register_key; - window[register_key] = this; - }, - - /** - * This must be called before any other method in order to initialize the lib. - * It provides the root of the rendering cycle as a Javascript object. - * @param {Object} renderCycleRoot A JS component with a render method. - */ - setRenderCycleRoot(renderCycleRoot) { - this.renderCycleRoot = renderCycleRoot; - }, - - event_name: "objtohtml-render-cycle", - - /** - * Set a custom event name for the event that is trigger on render cycle. - * Default is "objtohtml-render-cycle". - * @param {String} evt_name - */ - setEventName(evt_name) { - this.event_name = evt_name; - }, - - /** - * This is the core agorithm that read an javascript Object and convert it into an HTML element. - * @param {Object} obj The object representing the html element must be formatted like: - * { - * tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li... - * xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag. - * See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information - * style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax, - * like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc. - * contents: Array or String // This reprensents the contents that will be nested in the created html element. - * <div>{contents}</div> - * The contents can be an array of other objects reprenting elements (with tag, contents, etc) - * or it can be a simple string. - * // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title... - * // or they can also define custom html5 attributes, like data, my_custom_attr or anything. - * } - * @returns {HTMLElement} The output html node. - */ - objectToHtml(obj) { - if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process. - const objectToHtml = this.objectToHtml.bind(this); - const { tag, xmlns } = obj; - const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag); - const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"]; - - Object.keys(obj) - .filter(attr => !excludeKeys.includes(attr)) - .forEach(attr => { - switch (attr) { - case "class": - node.classList.add(...obj[attr].split(" ").filter(s => s !== "")); - break; - case "on_render": - if (!obj.id) { - node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`; - } - if (typeof obj.on_render !== "function") { - console.error("The on_render attribute must be a function") - } else { - this.attach_on_render_callback(node, obj.on_render); - } - break; - default: - if (xmlns !== undefined) { - node.setAttributeNS(null, attr, obj[attr]) - } else { - node[attr] = obj[attr]; - } - } - }); - if (obj.contents && typeof obj.contents === "string") { - node.innerHTML = obj.contents; - } else { - obj.contents && - obj.contents.length > 0 && - obj.contents.forEach(el => { - switch (typeof el) { - case "string": - node.innerHTML = el; - break; - case "object": - if (xmlns !== undefined) { - el = Object.assign(el, { xmlns }) - } - node.appendChild(objectToHtml(el)); - break; - } - }); - } - - if (obj.style_rules) { - Object.keys(obj.style_rules).forEach(rule => { - node.style[rule] = obj.style_rules[rule]; - }); - } - - return node; - }, - - on_render_callbacks: [], - - /** - * This is called if the on_render attribute of a component is set. - * @param {HTMLElement} node The created html element - * @param {Function} callback The callback defined in the js component to render - */ - attach_on_render_callback(node, callback) { - const callback_handler = { - callback: e => { - if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) { - callback(node); - const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node))); - if (handler_index === -1) { - console.warn("A callback was registered for node with id " + node.id + " but callbacck handler is undefined.") - } else { - window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback) - this.on_render_callbacks.splice(handler_index, 1); - } - } - }, - node, - }; - - const len = this.on_render_callbacks.push(callback_handler); - window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback); - }, - - /** - * If a main element exists in the html document, it will be used as rendering root. - * If not, it will be created and inserted. - */ - renderCycle: function () { - const main_elmt = document.getElementsByTagName("main")[0] || (function () { - const created_main = document.createElement("main"); - document.body.appendChild(created_main); - return created_main; - })(); - - this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" }); - }, - - /** - * This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component, - * it can start from any component of the tree. The root component must be given as the first argument, the second argument be - * be a valid html element in the dom and will be used as the insertion target. - * @param {Object} object An object providing a render method returning an object representation of the html to insert - * @param {HTMLElement} htmlNode The htlm element to update - * @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override", - * "insert-before" (must be defined along with an insertIndex key (integer)), - * "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove". - * In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}... - */ - subRender(object, htmlNode, options = { mode: "append" }) { - let outputNode = null; - - const get_insert = () => { - outputNode = this.objectToHtml(object); - return outputNode; - }; - - switch (options.mode) { - case "append": - htmlNode.appendChild(get_insert()); - break; - case "override": - htmlNode.innerHTML = ""; - htmlNode.appendChild(get_insert()); - break; - case "insert-before": - htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]); - break; - case "adjacent": - /** - * options.insertLocation must be one of: - * - * afterbegin - * afterend - * beforebegin - * beforeend - */ - htmlNode.insertAdjacentHTML(options.insertLocation, get_insert()); - break; - case "replace": - htmlNode.parentNode.replaceChild(get_insert(), htmlNode); - break; - case "remove": - htmlNode.remove(); - break; - } - const evt_name = this.event_name; - const event = new CustomEvent(evt_name, { - detail: { - inputObject: object, - outputNode, - insertOptions: options, - targetNode: htmlNode, - } - }); - - window.dispatchEvent(event); - }, -}; -},{}],37:[function(require,module,exports){ -"use strict"; - -class ImageCarousel { - constructor(props) { - this.props = props; - this.id = this.props.images.join("").replace(/\s\./g); - this.state = { - showImageIndex: 0, - }; - this.RUN_INTERVAL = 5000; - this.props.images.length > 1 && this.run(); - } - - run() { - this.runningInterval = setInterval(() => { - let { showImageIndex } = this.state; - const { images } = this.props; - this.state.showImageIndex = showImageIndex < images.length - 1 ? ++showImageIndex : 0; - this.refreshImage(); - }, this.RUN_INTERVAL); - } - - setImageIndex(i) { - clearInterval(this.runningInterval); - this.state.showImageIndex = i; - this.refreshImage(); - } - - refreshImage() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { - mode: "replace", - }); - } - - render() { - const { showImageIndex } = this.state; - const { images } = this.props; - return { - tag: "div", - id: this.id, - class: "image-carousel", - contents: [ - { - tag: "img", - property: "image", - alt: `image carousel ${images[showImageIndex].replace(/\.[A-Za-z]+/, "")}`, - src: images[showImageIndex], - }, - images.length > 1 && { - tag: "div", - class: "carousel-bullets", - contents: images.map((_, i) => { - const active = showImageIndex === i; - return { - tag: "span", - class: `bullet ${active ? "active" : ""}`, - onclick: this.setImageIndex.bind(this, i), - }; - }), - }, - ], - }; - } -} - -module.exports = ImageCarousel; - -},{}],38:[function(require,module,exports){ -"use strict"; - -const { fetch_json_or_error_text } = require("./fetch"); - -function getArticleBody(text) { - return text.replaceAll("\n", "<br/>"); -} - -function getArticleDate(date) { - return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; -} - -function loadArticles(category, locale) { - return fetch_json_or_error_text(`/articles/${category}/${locale}`); -} - -module.exports = { - loadArticles, - getArticleBody, - getArticleDate, -}; - -},{"./fetch":39}],39:[function(require,module,exports){ -"use strict"; - -function fetchjson(url) { - return new Promise((resolve, reject) => { - fetch(url) - .then(r => r.json()) - .then(r => resolve(r)) - .catch(e => reject(e)); - }); -} - -function fetchtext(url) { - return new Promise((resolve, reject) => { - fetch(url) - .then(r => r.text()) - .then(r => resolve(r)) - .catch(e => reject(e)); - }); -} - -async function fetch_json_or_error_text(url, options = {}) { - return new Promise((resolve, reject) => { - fetch(url, options).then(async res => { - if (res.status >= 400 && res.status < 600) { - reject(await res.text()); - } else { - resolve(await res.json()); - } - }) - }) -} - -module.exports = { - fetchjson, - fetchtext, - fetch_json_or_error_text, -}; - -},{}],40:[function(require,module,exports){ -"use strict"; -const translator = require("ks-cheap-translator"); -const { translations_url } = require("../../constants"); - -class WebPage { - constructor(args) { - Object.assign(this, args); - - if (!this.id) { - this.id = "webpage-" + performance.now(); - } - - translator.init({ - translations_url, - supported_languages: ["fr", "en"], - }).then(this.refresh_all.bind(this)); - } - - refresh() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) - } - - refresh_all() { - obj2htm.renderCycle() - } -} - -module.exports = WebPage; -},{"../../constants":3,"ks-cheap-translator":4}],41:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../../../admin-frontend/src/constants"); -const { data_url } = require("../../../../constants"); -const ImageCarousel = require("../../../generic-components/image-carousel"); -const { getArticleBody } = require("../../../lib/article-utils"); -const { fetch_json_or_error_text } = require("../../../lib/fetch"); -const { MentaloEngine } = require("mentalo-engine"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -class GameArticle { - constructor(props) { - this.props = props; - this.parse_body(); - } - - parse_body() { - let body = getArticleBody(this.props.body); - const play_btn_regex = /\[PLAY_BUTTON\s\{.+\}\]/g; - const found_play_buttons = body.match(play_btn_regex); - if (found_play_buttons) { - this.build_play_button(JSON.parse(found_play_buttons[0].replace(/[\[\]PLAY_BUTTON\s]/g, ""))); - body = body.replace(play_btn_regex, ""); - } - this.body = body; - } - - build_play_button(button_data) { - this.render_play_button = { - tag: "button", - class: "play-button", - contents: t("Jouer"), - onclick: this.handle_click_play.bind(this, button_data.filename, button_data.engine) - }; - } - - load_and_run_mentalo_game(filename, button_element) { - const button_text = button_element.innerHTML; - button_element.innerHTML = "Loading ..."; - button_element.style.pointerEvents = "none"; - - fetch_json_or_error_text(`${data_url}/${filename}`) - .then(game_data => { - const container = document.createElement("div"); - container.style.position = "fixed"; - container.style.top = 0; - container.style.left = 0; - container.style.right = 0; - container.style.bottom = 0; - container.style.zIndex = 10; - container.style.display = "flex"; - container.style.justifyContent = "center"; - container.style.alignItems = "center"; - - container.id = "kuadrado-tmp-game-player-container"; - document.body.appendChild(container); - document.body.style.overflow = "hidden"; - - const engine = new MentaloEngine({ - game_data, - fullscreen: true, - frame_rate: 30, - container, - on_quit_game: () => { - container.remove(); - document.body.style.overflow = "visible"; - } - }); - - engine.init(); - engine.run_game(); - }) - .catch(err => console.log(err)) - .finally(() => { - button_element.innerHTML = button_text; - button_element.style.pointerEvents = "unset"; - }); - } - - handle_click_play(filename, engine, e) { - switch (engine) { - case "mentalo": - this.load_and_run_mentalo_game(filename, e.target); - break; - default: - console.log("Error, unkown engine") - return; - } - } - - render() { - const { - title, - subtitle, - images, - details, - } = this.props; - - return { - tag: "article", - typeof: "VideoGame", - additionalType: "Article", - class: "game-article", - contents: [ - { - tag: "h2", - property: "name", - class: "game-title", - contents: title, - }, - { - tag: "div", - class: "game-banner", - contents: [ - { tag: "img", class: "pixelated", src: `${images_url}/${images[0]}` }, - ], - }, - { - tag: "h3", - class: "game-subtitle", - contents: subtitle, - property: "alternativeHeadline", - }, - { - tag: "div", - class: "game-description", - property: "description", - contents: [{ tag: "p", style_rules: { margin: 0 }, contents: this.body }] - .concat(this.render_play_button - ? [this.render_play_button] - : []), - }, - new ImageCarousel({ images: images.map(img => `${images_url}/${img}`) }).render(), - details.length > 0 && { - tag: "div", - class: "article-details", - contents: [ - { - tag: "h2", - contents: "Details", - }, - { - tag: "ul", - class: "details-list", - contents: details.map(detail => { - return { - tag: "li", - class: "detail", - contents: [ - { tag: "label", contents: detail.label }, - { - tag: "div", - class: "detail-value", - contents: detail.value - }, - ], - }; - }), - }, - ], - }, - ], - }; - } -} - -module.exports = GameArticle; - -},{"../../../../../admin-frontend/src/constants":1,"../../../../constants":3,"../../../generic-components/image-carousel":37,"../../../lib/article-utils":38,"../../../lib/fetch":39,"ks-cheap-translator":4,"mentalo-engine":5}],42:[function(require,module,exports){ -"use strict"; - -const { loadArticles } = require("../../../lib/article-utils"); -const GameArticle = require("./game-article"); -const translator = require("ks-cheap-translator"); - -class GameArticles { - constructor(props) { - this.props = props; - this.state = { - articles: [], - }; - this.id = "game-articles-section"; - this.loadArticles(); - } - - loadArticles() { - loadArticles("games", translator.locale) - .then(articles => { - this.state.articles = articles; - this.refresh(); - }) - .catch(e => console.log(e)); - } - - renderPlaceholder() { - return { - tag: "article", - class: "placeholder", - contents: [{ tag: "div" }, { tag: "div" }], - }; - } - - refresh() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { - mode: "replace", - }); - } - - render() { - const { articles } = this.state; - return { - tag: "section", - class: "game-articles page-contents-center", - id: this.id, - contents: - articles.length > 0 - ? articles.map(article => new GameArticle({ ...article }).render()) - : [this.renderPlaceholder()], - }; - } -} - -module.exports = GameArticles; - -},{"../../../lib/article-utils":38,"./game-article":41,"ks-cheap-translator":4}],43:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const WebPage = require("../../lib/web-page"); -const GameArticles = require("./components/game-articles"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -class GamesPage extends WebPage { - render() { - return { - tag: "div", - id: "games-page", - contents: [ - { - tag: "div", - class: "page-header logo-left", - contents: [ - { - tag: "div", - class: "page-contents-center grid-wrapper", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: "image game controller", - src: `${images_url}/game_controller.svg`, - }, - ], - }, - { tag: "h1", contents: t("Jeux") }, - { - tag: "p", - contents: t("games-page-intro"), - }, - ], - }, - ], - }, - new GameArticles().render(), - ], - }; - } -} - -module.exports = GamesPage; - -},{"../../../constants":3,"../../lib/web-page":40,"./components/game-articles":42,"ks-cheap-translator":4}],44:[function(require,module,exports){ -"use strict"; - -"use strict"; -const runPage = require("../../run-page"); -const GamesPage = require("./games"); -runPage(GamesPage); - -},{"../../run-page":45,"./games":43}],45:[function(require,module,exports){ -"use strict"; - -const renderer = require("object-to-html-renderer") -const Template = require("./template/template"); - -module.exports = function runPage(PageComponent) { - const template = new Template({ page: new PageComponent() }); - renderer.register("obj2htm") - obj2htm.setRenderCycleRoot(template); - obj2htm.renderCycle(); -}; - -},{"./template/template":47,"object-to-html-renderer":36}],46:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -const NAV_MENU_ITEMS = [ - { url: "/games/", text: "Jeux" }, - { - url: "/education/", - text: "Pédagogie", - }, - { url: "/software-development/", text: "Software" } -]; - -class NavBar { - constructor() { - this.initEventHandlers(); - } - - handleBurgerClick() { - document.getElementById("nav-menu-list").classList.toggle("responsive-show"); - } - - initEventHandlers() { - window.addEventListener("click", event => { - if ( - event.target.id !== "nav-menu-list" && - !event.target.classList.contains("burger") && - !event.target.parentNode.classList.contains("burger") - ) { - document.getElementById("nav-menu-list").classList.remove("responsive-show"); - } - }); - } - - handle_chang_lang(lang) { - translator.update_translations(lang).then(() => { - obj2htm.renderCycle(); - }).catch(err => console.log(err)); - } - - renderHome() { - return { - tag: "div", - class: "home", - contents: [ - { - tag: "a", - href: "/", - contents: [ - { - tag: "img", - alt: "Logo Kuadrado", - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - alt: "Kuadrado Software", - class: "logo-text", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - ], - }; - } - - renderMenu(menuItemsArray, isSubmenu = false, parentUrl = "") { - return { - tag: "ul", - id: "nav-menu-list", - class: isSubmenu ? "submenu" : "", - contents: menuItemsArray.map(item => { - const { url, text, submenu } = item; - const href = `${parentUrl}${url}`; - return { - tag: "li", - class: !isSubmenu && window.location.pathname === href ? "active" : "", - contents: [ - { - tag: "a", - href, - contents: t(text), - }, - ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), - }; - }).concat({ - tag: "li", - class: "lang-flags", - contents: ["fr", "en"].map(lang => { - return { - tag: "img", src: `${images_url}/flag-${lang}.svg`, - class: translator.locale === lang ? "selected" : "", - onclick: this.handle_chang_lang.bind(this, lang) - } - }) - }), - }; - } - - renderResponsiveBurger() { - return { - tag: "div", - class: "burger", - onclick: this.handleBurgerClick.bind(this), - contents: [{ tag: "span", contents: "···" }], - }; - } - - render() { - return { - tag: "nav", - contents: [ - this.renderHome(), - this.renderResponsiveBurger(), - this.renderMenu(NAV_MENU_ITEMS), - ], - }; - } -} - -module.exports = NavBar; - -},{"../../../constants":3,"ks-cheap-translator":4}],47:[function(require,module,exports){ -"use strict"; - -const { in_construction } = require("../../config"); -const { images_url } = require("../../constants"); -const NavBar = require("./components/navbar"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator) - -class Template { - constructor(props) { - this.props = props; - } - render() { - return { - tag: "main", - contents: [ - { - tag: "header", - contents: [new NavBar().render()], - }, - in_construction && { - tag: "section", - class: "warning-banner", - contents: [ - { - tag: "strong", - class: "page-contents-center", - contents: t("Site en construction ..."), - }, - ], - }, - { - tag: "section", - id: "page-container", - contents: [this.props.page.render()], - }, - { - tag: "footer", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: `logo Kuadrado`, - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - class: "text-logo", - alt: "Kuadrado Software", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - { - tag: "span", - contents: "32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France", - }, - { - tag: "div", - contents: [ - { tag: "strong", contents: "<blue>Contact : </blue>" }, - { - tag: "a", - href: "mailto:contact@kuadrado-software.fr", - contents: "contact@kuadrado-software.fr", - }, - ], - }, - { - tag: "div", - class: "social", - contents: [ - { - tag: "strong", - contents: `<blue>${t("Sur les réseaux")} : </blue>`, - }, - { - tag: "a", - href: "https://www.linkedin.com/company/kuadrado-software", - target: "_blank", - contents: "in", - title: "Linkedin", - }, - { - tag: "a", - href: "https://mastodon.gamedev.place/@kuadrado_software", - target: "_blank", - contents: "m", - title: "Mastodon", - } - ], - }, - { - tag: "span", - contents: `Copyleft 🄯 ${new Date() - .getFullYear()} Kuadrado Software | - ${t("kuadrado-footer-copyleft")}`, - }, - { - tag: "div", contents: [ - { tag: "span", contents: t("Ce site web est") + " " }, - { - tag: "a", target: "_blank", - style_rules: { fontWeight: "bold" }, - href: "https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md", - contents: "OPEN SOURCE" - } - ] - } - ], - }, - ], - }; - } -} - -module.exports = Template; - -},{"../../config":2,"../../constants":3,"./components/navbar":46,"ks-cheap-translator":4}]},{},[44]); +!function s(i,o,a){function r(t,e){if(!o[t]){if(!i[t]){var n="function"==typeof require&&require;if(!e&&n)return n(t,!0);if(c)return c(t,!0);throw(n=new Error("Cannot find module '"+t+"'")).code="MODULE_NOT_FOUND",n}n=o[t]={exports:{}},i[t][0].call(n.exports,function(e){return r(i[t][1][e]||e)},n,n.exports,s,i,o,a)}return o[t].exports}for(var c="function"==typeof require&&require,e=0;e<a.length;e++)r(a[e]);return r}({1:[function(e,t,n){t.exports={images_url:"/assets/images"}},{}],2:[function(e,t,n){t.exports={build:{protected_dirs:["assets","style","views","standard"],default_meta_keys:["title","description","image","open_graph","json_ld"]}}},{}],3:[function(e,t,n){t.exports={images_url:"/assets/images",data_url:"/assets/data",translations_url:"/assets/translations"}},{}],4:[function(e,t,n){"use strict";t.exports={locale:"en",supported_languages:["en"],translations:{},translations_url:"",use_url_locale_fragment:!0,local_storage_key:"translator-prefered-language",init(e){return Object.entries(e).forEach(e=>{var[t,e]=e;["supported_languages","use_url_locale_fragment","local_storage_key","translations_url"].includes(t)&&(this[t]=e)}),this.translations_url=this.format_translations_url(this.translations_url),this.supported_languages=this.format_supported_languages(this.supported_languages),new Promise((t,n)=>{const s=(()=>{if(this.use_url_locale_fragment){var e=window.location.pathname.substring(1).split("/")[0];return this.supported_languages.includes(e)?e:""}return""})()||localStorage.getItem(this.local_storage_key)||(e=navigator.language.split("-")[0].toLocaleLowerCase(),this.supported_languages.includes(e)?e:this.supported_languages[0]);var e;fetch(`${this.translations_url}${s}.json`).then(e=>e.json()).then(e=>{this.locale=s,this.translations=e,t()}).catch(e=>{this.locale="en",n(e)})})},format_locale(e){return e.split("-")[0].toLowerCase()},format_translations_url(e){return"/"!==e.charAt(e.length-1)&&(e+="/"),e},format_supported_languages(e){return e.map(e=>this.format_locale(e))},update_translations(s){return s=this.format_locale(s),new Promise((n,t)=>{fetch(`${this.translations_url}${s}.json`).then(e=>e.json()).then(e=>{this.translations=e,this.locale=s,localStorage.setItem(this.local_storage_key,s);const t=window.location.pathname.substring(1).split("/");e=t[0];this.supported_languages.includes(e)&&(t.splice(0,1,s),e=t.join("/"),window.history.replaceState(null,"","/"+e)),n()}).catch(e=>{t(e)})})},trad:function(t,n={}){return t=this.translations[t]||t,Object.keys(n).forEach(e=>{t=t.replace(`{%${e}%}`,n[e])}),t}}},{}],5:[function(e,t,n){"use strict";var s=e("./model/animation"),i=e("./mentalo-engine"),o=e("./model/scene"),a=e("./model/game"),r=e("./model/sound-track"),c=e("./model/scene-types"),l=e("./model/choice"),_=e("./model/game-object"),h=e("./lib/frame-rate-controller"),d=e("./lib/font-tools"),m=e("./lib/color-tools"),e=e("./lib/shape-tools");t.exports={MentaloEngine:i,MtlAnimation:s,MtlScene:o,MtlGame:a,MtlSoundTrack:r,SCENE_TYPES:c,MtlChoice:l,MtlGameObject:_,FrameRateController:h,font_tools:d,color_tools:m,shape_tools:e}},{"./lib/color-tools":6,"./lib/font-tools":7,"./lib/frame-rate-controller":8,"./lib/shape-tools":9,"./mentalo-engine":11,"./model/animation":12,"./model/choice":13,"./model/game":15,"./model/game-object":14,"./model/scene":19,"./model/scene-types":18,"./model/sound-track":20}],6:[function(e,t,n){"use strict";function s(e){return(e=e.toLowerCase(0)).includes("rgb")?e.replaceAll(/[a-z\(\)w]/g,"").split(",").slice(0,3).map(e=>parseInt(e)):e.replace("#","").match(/.{0,2}/g).slice(0,3).map(e=>parseInt(e,16))}function i(e){return s(e).reduce((e,t)=>e+t/3,0)}function o(e){return`#${e.slice(0,3).map(e=>{e=e.toString(16);return e.length<2?"0"+e:e}).join("")}`}t.exports={get_average_rgb_color_tone:i,get_optimal_visible_foreground_color:function(e){const t=document.createElement("canvas");t.width=1,t.height=1;const n=t.getContext("2d");return n.fillStyle=e,n.fillRect(0,0,1,1),e=127<i(e)?"#0000003f":"#ffffff3f",n.fillStyle=e,n.fillRect(0,0,1,1),o(Array.from(n.getImageData(0,0,1,1).data))},color_str_to_rgb_array:s,rgb_array_to_hex:o,rgba_array_to_hex:function(e){return`#${(e=4===e.length?e:e.concat([255])).slice(0,4).map(e=>{e=e.toString(16);return e.length<2?"0"+e:e}).join("")}`},same_rgba:function(e,t){return e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3]}}},{}],7:[function(e,t,n){"use strict";const s=[{category:"Sans serif",values:[{value:"sans-serif",text:"sans-serif"},{text:"Arial",value:"Arial, sans-serif"},{text:"Helvetica",value:"Helvetica, sans-serif"},{text:"Verdana",value:"Verdana, sans-serif"},{text:"Trebuchet MS",value:"Trebuchet MS, sans-serif"},{text:"Noto Sans",value:"Noto Sans, sans-serif"},{text:"Gill Sans",value:"Gill Sans, sans-serif"},{text:"Avantgarde",value:"Avantgarde, TeX Gyre Adventor, URW Gothic L, sans-serif"},{text:"Optima",value:"Optima, sans-serif"},{text:"Arial Narrow",value:"Arial Narrow, sans-serif"}]},{category:"Serif",values:[{text:"serif",value:"serif"},{text:"Times",value:"Times, Times New Roman, serif"},{text:"Didot",value:"Didot, serif"},{text:"Georgia",value:"Georgia, serif"},{text:"Palatino",value:"Palatino, URW Palladio L, serif"},{text:"Bookman",value:"Bookman, URW Bookman L, serif"},{text:"New Century Schoolbook",value:"New Century Schoolbook, TeX Gyre Schola, serif"},{text:"American Typewriter",value:"American Typewriter, serif"}]},{category:"Monospace",values:[{text:"monospace",value:"monospace"},{text:"Andale Mono",value:"Andale Mono, monospace"},{text:"Courrier New",value:"Courier New, monospace"},{text:"Courrier",value:"Courier, monospace"},{text:"FreeMono",value:"FreeMono, monospace"},{text:"OCR A Std",value:"OCR A Std, monospace"},{text:"DejaVu Sans Mono",value:"DejaVu Sans Mono, monospace"}]},{category:"Cursive",values:[{text:"cursive",value:"cursive"},{text:"Comic Sans MS",value:"Comic Sans MS, Comic Sans, cursive"},{text:"Apple Chancery",value:"Apple Chancery, cursive"},{text:"Bradley Hand",value:"Bradley Hand, cursive"},{text:"Brush Script MT",value:"Brush Script MT, Brush Script Std, cursive"},{text:"Snell Roundhand",value:"Snell Roundhand, cursive"},{text:"URW Chancery L",value:"URW Chancery L, cursive"}]}],i=[{value:"left",text:"Left"},{value:"right",text:"Right"},{value:"center",text:"Center"}],o=[{value:"normal",text:"Normal"},{value:"italic",text:"Italic"}],a=[{value:"1",text:"Thin"},{value:"normal",text:"Normal"},{value:"900",text:"Bold"}];function r(e={}){const{font_size:t=20,font_family:n="sans-serif",font_style:s="normal",font_weight:i="normal"}=e;return`${s} normal ${i} ${t.toFixed()}px ${n}`}t.exports={get_canvas_font:r,get_font_options:function(){return{font_families:s,text_align_options:i,font_style_options:o,font_weight_options:a}},get_canvas_char_size:function(e,t){e.save(),e.font=r(t);var n="Lorem ipsum dolor Sit amet, Consectetur adipiscing elit",s=e.measureText(n).width/n.length,n=(t=1.1*e.measureText("M").width)+t/4;return e.restore(),{width:s,height:t,text_line_height:n,interline_height:t/4}},FONT_FAMILIES:s,TEXT_ALIGN_OPTIONS:i,FONT_STYLE_OPTIONS:o,FONT_WEIGHT_OPTIONS:a}},{}],8:[function(e,t,n){"use strict";t.exports=class{constructor(e){this.tframe=performance.now(),this.interval=1e3/e,this.initial=!0}nextFrameReady(){if(this.initial)return!(this.initial=!1);var e=performance.now(),t=e-this.tframe,n=t>this.interval;return n&&(this.tframe=e-t%this.interval),n}}},{}],9:[function(e,t,n){"use strict";t.exports={draw_rect:function(e,t,n,s,i,o={rounded_corners_radius:0,border:{width:0,color:"rgba(0,0,0,0)"},fill_color:"black"}){var{rounded_corners_radius:a=0,border:r={width:0,color:"rgba(0,0,0,0)"},fill_color:c="black",fill_image:l}=o,a=(o=Math.min(s,i))/2<a?o/2:a;e.save(),e.beginPath(),e.arc(t+a,n+a,a,Math.PI,3*Math.PI/2),e.lineTo(t+s-a,n),e.arc(t+s-a,n+a,a,3*Math.PI/2,0),e.lineTo(t+s,n+i-a),e.arc(t+s-a,n+i-a,a,0,Math.PI/2),e.lineTo(t+a,n+i),e.arc(t+a,n+i-a,a,Math.PI/2,Math.PI),e.closePath(),e.clip(),l?e.drawImage(l.src,0,0,l.src.width,l.src.height,l.dx,l.dy,l.dw,l.dh):(e.fillStyle=c,e.fillRect(t,n,s,i)),0<r.width&&(e.strokeStyle=r.color,e.lineWidth=r.width,e.stroke()),e.restore()}}},{}],10:[function(e,t,n){"use strict";const{get_canvas_font:l,get_canvas_char_size:_}=e("./font-tools");t.exports={draw_text_in_bounds:function(i,s,o,a,r,e){i.save();const t=_(i,a).text_line_height;i.font=l(a),i.fillStyle=a.font_color,i.textAlign=a.text_align,i.textBaseline="top";const n=(()=>{var{padding:e=0}=a,{left:t,top:n,width:s}=o;switch(i.textAlign){case"left":return{x:t+e,y:n+e};case"right":return{x:t+s-e,y:n+e};case"center":return{x:t+s/2,y:n+e};default:return{x:t+e,y:n+e}}})();r.stream=r.stream||{line_chars:0,line_index:0,complete:!1};const c=e?s.map((e,t)=>{const n=r.stream;if(t<n.line_index||n.complete)return e;if(t>n.line_index)return"";t=e.slice(0,++n.line_chars);return t.length===e.length&&(n.complete=s.length-1===n.line_index,n.line_index=n.complete?n.line_index:n.line_index+1,n.line_chars=0),t}):(r.stream.complete=!0,s);return c.forEach(e=>{i.fillText(e,n.x,n.y),n.y+=t}),i.restore(),r.stream.complete}}},{"./font-tools":7}],11:[function(e,t,n){"use strict";const s=e("./model/game"),i=e("./model/scene-types"),o=e("./render/render"),{supported_locales:a,get_translated:r}=e("./translation");t.exports=class{constructor(e){this.game=new s,this.game.load_data(e.game_data),this.game.on_load_resources(()=>this.loading=!1),this.use_locale=e.use_locale&&a.includes(e.use_locale)?e.use_locale:"en",this.escape_listener=e=>{e=e.key.toLowerCase();"escape"!==e&&"q"!==e||this.quit()},window.addEventListener("keydown",this.escape_listener),this.on_quit_game=function(){this.used_default_container&&this.container.remove(),window.removeEventListener("keydown",this.escape_listener),e.on_quit_game&&e.on_quit_game()},this.used_default_container=!1,this.container=e.container||(()=>{const e=document.createElement("div");return e.style.display="flex",e.style.justifyContent="center",e.style.alignItems="center",e.style.position="absolute",e.style.top=0,e.style.bottom=0,e.style.left=0,e.style.right=0,e.style.zIndex=1e3,e.style.overflow="hidden",document.body.appendChild(e),this.used_default_container=!0,e})(),this.render=new o({container:this.container,fullscreen:e.fullscreen,frame_rate:e.frame_rate,get_game_settings:this.get_game_settings.bind(this),get_game_scenes:this.get_game_scenes.bind(this),get_scene:this.get_scene.bind(this),get_scene_index:this.get_scene_index.bind(this),get_inventory:this.get_inventory.bind(this),on_game_object_click:this.on_game_object_click.bind(this),on_drop_inventory_object:this.on_drop_inventory_object.bind(this),on_choice_click:this.on_choice_click.bind(this)}),this.loading=!0,this.will_quit=!1}get_game_settings(e){return this.game.game_ui_options[e]}get_game_scenes(){return this.game.scenes}get_scene(){return this.game.get_scene()}get_scene_index(){return this.game.scenes.indexOf(this.get_scene())}get_inventory(){return this.game.state.inventory}quit(){this.will_quit=!0}__quit(){window.cancelAnimationFrame(window.mentalo_engine_animation_id),this.render.exit_fullscreen(),this.render.clear_event_listeners(),this.game.get_soundtrack().stop(),this.on_quit_game()}init(){window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,window.cancelAnimationFrame=window.cancelAnimationFrame||window.mozCancelAnimationFrame,window.mentalo_engine_animation_id&&window.cancelAnimationFrame(window.mentalo_engine_animation_id),this.render.init(),this.game.scenes.find(e=>e.animation.empty)&&(alert(r("Some scenes have an empty image, game cannot be loaded",this.use_locale)),this.quit())}go_to_scene(e){this.render.reset_user_error_popup(),this.render.clear_event_listeners(),this.game.get_soundtrack().stop(),this.game.go_to_scene(e),this.render.clear_children(),this.render.reset_text_box_visibility()}on_game_object_click(e){this.game.inventory_has_empty_slot()&&this.game.add_object_to_inventory(e)}on_drop_inventory_object(e){this.game.remove_object_from_inventory(e)}on_choice_click(e){const{destination_scene_index:t,use_objects:n}=e;if(-1!==t)if(-2!==t){if(n.value){var s=this.get_inventory();const o=[];for(const a of n.items){var i=Array.from(s).find(e=>e.name===a.name);if(!i)return void this.render.set_user_error_popup({text:n.missing_object_message,stream_text:!0});o.push(i)}o.forEach(t=>n.items.find(e=>t.name===e.name).consume_object&&this.game.consume_game_object(t))}this.go_to_scene(t)}else this.quit();else this.render.set_user_error_popup({text:r("Destination scene has not been set.",this.use_locale)})}on_cinematic_end(){var e=this.game.get_scene();e.end_cinematic_options.quit?this.quit():-1===e.end_cinematic_options.destination_scene_index?this.render.set_user_error_popup({text:r("Next scene has not been set.",this.use_locale),on_close:this.quit.bind(this)}):this.go_to_scene(e.end_cinematic_options.destination_scene_index)}run_game(){if(this.will_quit)this.__quit();else{if(this.loading)this.render.draw_loading();else{this.get_scene()._type===i.CINEMATIC&&(this.game.update_cinematic_timeout(),this.game.is_cinematic_ended()&&this.on_cinematic_end());const e=this.game.get_soundtrack();e.loaded&&!e.is_playing()&&e.play(),this.render.draw_game()}window.mentalo_engine_animation_id=requestAnimationFrame(this.run_game.bind(this))}}}},{"./model/game":15,"./model/scene-types":18,"./render/render":23,"./translation":35}],12:[function(e,t,n){"use strict";class s extends e("./loadable"){constructor(){super(new Image,"image","load"),this.image.onload=()=>this.update_dimensions(),this.name="",this.dimensions={width:0,height:0},this.frame_nb=1,this.frame=0,this.speed=1,this.play_once=!1,this.initialized=!1,this.finished=!1}update_dimensions(){this.dimensions={width:this.image.width/this.frame_nb,height:this.image.height},this.canvas_precalc&&delete this.canvas_precalc}init(e){this.empty=""===e.src,this.name=e.name,this.image.src=e.src,this.frame_nb=e.frame_nb,this.frame=0,this.speed=e.speed,this.play_once=e.play_once,this.initialized=!0}update_frame(e){this.finished=this.play_once&&this.frame===this.frame_nb-1,1<this.frame_nb&&e%this.speed==0&&!this.finished&&(this.frame=this.frame+1<=this.frame_nb-1?this.frame+1:0)}reset_frame(){this.finished=!1,this.frame=0}}t.exports=s},{"./loadable":17}],13:[function(e,t,n){"use strict";t.exports=class{constructor(e={text:"",destination_scene_index:-1,use_objects:{value:!1,items:[],missing_object_message:""}}){this.load_data(e)}load_data(e){this.text=e.text,this.destination_scene_index=e.destination_scene_index,this.use_objects=e.use_objects}}},{}],14:[function(e,t,n){"use strict";class s extends e("./loadable"){constructor(){super(new Image,"image","load"),this.name="",this.position={x:0,y:0},this.state={}}load_data(e){this.image.src=e.image,this.name=e.name,this.position=e.position}get_dimensions(){return{width:this.image.width,height:this.image.height}}get_bounds(){var e=this.get_dimensions();return{top:this.position.y,right:this.position.x+e.width,bottom:this.position.y+e.height,left:this.position.x}}}t.exports=s},{"./loadable":17}],15:[function(e,t,n){"use strict";const s=e("./scene"),i=e("./scene-types");t.exports=class{constructor(){this.name="",this.scenes=[new s],this.game_ui_options={general:{background_color:"#000000",animation_canvas_dimensions:{width:600,ratio:"4:3",height:function(){var e=this.ratio.split(":").map(e=>parseInt(e)),e=e[1]/e[0];return Number((this.width*e).toFixed())}}},text_boxes:{background_color:"#222222",font_size:20,font_style:"normal",font_weight:"normal",font_family:"Arial, sans-serif",font_color:"#ffffff",text_align:"left",padding:20,margin:20,border_width:0,rounded_corners_radius:0},choices_panel:{background_color:"#222222",font_size:20,font_family:"sans",font_color:"#ffffff",font_style:"normal",text_align:"left",font_weight:"normal",container_padding:20,choice_padding:10,active_choice_background_color:"rgba(255,255,255,.4)",active_choice_border_width:0,active_choice_rounded_corners_radius:0},inventory:{background_color:"#222222",columns:1,rows:4,gap:10,padding:20,slot_rounded_corner_radius:0,slot_border_width:2}},this.starting_scene_index=0,this.state={scene:this.starting_scene_index,inventory:new Set,cinematic_timeout:{inc:0,last_update_time:-1,timeout:!1}},this.load_state={loaded_scenes:0}}get_scene(){return this.scenes[this.state.scene]}get_soundtrack(){return this.get_scene().sound_track}go_to_scene(e){const t=this.get_scene();t.animation.reset_frame(),t._type==i.CINEMATIC&&(this.state.cinematic_timeout={inc:0,last_update_time:-1,timeout:!1}),this.state.scene=e}update_cinematic_timeout(){var e;this.state.cinematic_timeout.timeout||(e=(new Date).getTime(),-1===this.state.cinematic_timeout.last_update_time&&(this.state.cinematic_timeout.last_update_time=e),this.state.cinematic_timeout.inc=e-this.state.cinematic_timeout.last_update_time,this.state.cinematic_timeout.timeout=1e3*this.get_scene().cinematic_duration<=this.state.cinematic_timeout.inc)}is_cinematic_ended(){var e=this.get_scene();return 0===e.cinematic_duration?e.animation.finished:this.state.cinematic_timeout.timeout}add_object_to_inventory(e){this.state.inventory.add(e)}inventory_has_empty_slot(){var{columns:e,rows:t}=this.game_ui_options.inventory;return this.state.inventory.size<e*t}remove_object_from_inventory(e){this.state.inventory.delete(e)}consume_game_object(t){this.remove_object_from_inventory(t);for(const n of this.scenes){var e=n.game_objects.find(e=>e===t);if(e){n.game_objects.splice(n.game_objects.indexOf(e),1);break}}}all_resources_loaded(){return this.load_state.loadable_elements===this.load_state.loaded_elements}get_game_objects(){return this.scenes.reduce((e,t)=>e.concat(t.game_objects),[])}load_data(e){this.name=e.name,this.starting_scene_index=e.starting_scene_index||0,this.state.scene=this.starting_scene_index,this.scenes=e.scenes.map(e=>{const t=new s;return t.load_data(e),t.on_load_group_complete(()=>{this.load_state.loaded_scenes++,this.load_state.loaded_scenes===this.scenes.length&&this.on_load_resources_callback&&this.on_load_resources_callback()}),t}),this.game_ui_options=Object.assign(e.game_ui_options,{general:{background_color:e.game_ui_options.general.background_color,animation_canvas_dimensions:Object.assign(e.game_ui_options.general.animation_canvas_dimensions,{height:function(){var e=this.ratio.split(":").map(e=>parseInt(e)),e=e[1]/e[0];return Number((this.width*e).toFixed())}})}})}on_load_resources(e){this.on_load_resources_callback=e}}},{"./scene":19,"./scene-types":18}],16:[function(e,t,n){"use strict";t.exports=class{constructor(){this.loadable_elements=0,this.loaded_elements=0}add_loadable(e){this.loadable_elements++,e.on_load(()=>{this.loaded_elements++,this.loadable_elements===this.loaded_elements&&this.on_load_group_complete_custom_callback&&this.on_load_group_complete_custom_callback()})}on_load_group_complete(e){this.on_load_group_complete_custom_callback=e}}},{}],17:[function(e,t,n){"use strict";t.exports=class{constructor(e,t,n){this.loaded=!1,this.init_loadable_element(e,t,n)}init_loadable_element(e,t,n){this[t]=e,this.load_listener=()=>{this.loaded=!0,this.on_load_callback(),this.clear()},this[t].addEventListener(n,this.load_listener),this.clear=()=>{this[t].removeEventListener(n,this.load_listener)}}on_load(e){this.on_load_custom_callback=e}on_load_callback(){this.loaded=!0,this.on_load_custom_callback&&this.on_load_custom_callback()}}},{}],18:[function(e,t,n){t.exports={PLAYABLE:"Playable",CINEMATIC:"Cinematic"}},{}],19:[function(e,t,n){"use strict";const s=e("./scene-types"),i=e("./animation"),o=e("./sound-track"),a=e("./choice"),r=e("./game-object");class c extends e("./loadable-group"){constructor(){super();var e={name:"",_type:s.PLAYABLE,animation:new i,sound_track:new o,choices:[],text_box:"",game_objects:[],cinematic_duration:0,end_cinematic_options:{destination_scene_index:-1,quit:!1}};this.name=e.name,this._type=e._type,this.animation=e.animation,this.sound_track=e.sound_track,this.choices=e.choices,this.text_box=e.text_box,this.game_objects=e.game_objects,this.cinematic_duration=e.cinematic_duration||0,this.end_cinematic_options=e.end_cinematic_options}load_data(e){this.name=e.name,this._type=e._type;const t=new i;t.init(e.animation),this.animation.clear(),this.animation=t,this.animation.image.src&&this.add_loadable(this.animation);const n=new o;n.init(e.sound_track),this.sound_track.clear(),this.sound_track=n,this.sound_track.src&&this.add_loadable(this.sound_track),this.choices=e.choices.map(e=>new a(e)),this.text_box=e.text_box,this.cinematic_duration=e.cinematic_duration||0,this.game_objects=e.game_objects.map(e=>{const t=new r;return t.load_data(e),this.add_loadable(t),t}),this.end_cinematic_options=e.end_cinematic_options}}t.exports=c},{"./animation":12,"./choice":13,"./game-object":14,"./loadable-group":16,"./scene-types":18,"./sound-track":20}],20:[function(e,t,n){"use strict";class s extends e("./loadable"){constructor(){super(new Audio,"audio","loadeddata"),this.name="",this.initialized=!1}reset(){this.init_loadable_element(new Audio,"audio","loadeddata"),this.name="",this.initialized=!1}init(e){this.empty=""===e.src,this.audio.src=e.src,this.name=e.name,this.audio.loop=e._loop,this.initialized=!0}play(){this.audio.play()}stop(){this.audio.pause(),this.audio.currentTime=0}is_playing(){return!this.audio.paused}}t.exports=s},{"./loadable":17}],21:[function(e,t,n){"use strict";const o=e("./choices-panel-metrics");t.exports=class{constructor(e){this.params=e,this.image={width:0,height:0},this.width=0,this.height=0,this.inventory={width:0,height:0},this.choices_panel={width:0,height:0},this.scale_factor=1,this.init_dimensions()}init_dimensions(){const{get_game_settings:e}=this.params,t=e("general");var n=t.animation_canvas_dimensions.height(),s=t.animation_canvas_dimensions.width,i=this.get_inventory_base_width(),o=s+i;this.choices_panel_metrics=this.get_choices_panel_metrics();var a,r=window.screen.width-200,c=window.screen.height-200,l=n+this.choices_panel_metrics.get_total_height(),_=o/l;_<window.screen.width/window.screen.height?this.width=(a=c*_)<=r?a:r:(this.height=(r=l/o*r)<=c?r:c,this.width=this.height*_),this.scale_factor=this.width/o,this.choices_panel_metrics=this.choices_panel_metrics.to_scaled(this.scale_factor),this.image={width:s*this.scale_factor,height:n*this.scale_factor},this.inventory={width:i*this.scale_factor,height:this.image.height},this.choices_panel={width:this.width,height:this.choices_panel_metrics.get_total_height()},this.height=this.image.height+this.choices_panel.height}get_inventory_base_width(){const{get_game_settings:e}=this.params;var t=e("general").animation_canvas_dimensions.height(),n=e("inventory"),s=n.gap,i=t-2*n.padding,t=(n.rows-1)*s,s=(n.columns-1)*s,t=(i-t)/n.rows;return n.columns*t+2*n.padding+s}get_choices_panel_metrics(){const{get_game_settings:e,get_game_scenes:t}=this.params;var n=e("choices_panel"),s=e("general"),i=n.container_padding,s=s.animation_canvas_dimensions.width+this.get_inventory_base_width()-2*i,i=t().map(e=>({choices:e.choices}));return new o({settings:n,container_width:s,scenes_choices:i})}}},{"./choices-panel-metrics":22}],22:[function(e,t,n){"use strict";const{get_canvas_char_size:o}=e("../lib/font-tools");t.exports=class t{constructor(e){this.params=e;var{scale_factor:e=1}=this.params;this.container_width=this.params.container_width*e,this.settings=this.get_scaled_settings(e),this.scenes_formatted_choices=this.get_scenes_formatted_choices(),this.container_padding_height=2*this.settings.container_padding,this.rows=this.get_rows_metrics()}get_scaled_settings(e){var{settings:t}=this.params;return Object.assign({...t},{font_size:t.font_size*e,active_choice_border_width:t.active_choice_border_width*e,active_choice_rounded_corners_radius:t.active_choice_rounded_corners_radius*e,choice_padding:t.choice_padding*e,container_padding:t.container_padding*e})}get_rows_metrics(e,t){var{settings:n}=this,s=this.get_choices_max_lines_per_row(e,t),e=(i=o(window.mentalo_drawing_context,n)).text_line_height,t=i.interline_height,i=2*n.choice_padding,n=s[0]*e-t,t=0<s[1]?s[1]*e-t:0;return[{text_height:n,padding_height:i},{text_height:t,padding_height:0<t?i:0}]}get_choices_max_lines_per_row(e,t){e=e||this.get_max_choices_row_per_scene();const i=t||this.scenes_formatted_choices,o=[0,0];return Array.from({length:e}).forEach((e,t)=>{const n=0===t?[0,2]:[2,4];var s=i.map(e=>Math.max(...e.slice(...n).map(e=>e.text_lines.length))).reduce((e,t)=>Math.max(e,t),0);o[t]=s}),o}get_max_choices_row_per_scene(e){const t=e?this.params.scenes_choices.slice(e,e+1):this.params.scenes_choices;let n=1;return 2<Math.max(...t.map(e=>e.choices.length))&&(n=2),n}get_scenes_formatted_choices(){const{scenes_choices:e}=this.params;var{settings:t,container_width:n}=this,{choice_padding:s}=t;const i=.9*(n/2-2*s)/o(window.mentalo_drawing_context,t).width;return e.map(e=>e.choices.map(e=>{const t=e.text.split(" "),n=[""];let s=0;return t.forEach(e=>{(n[s]+e).length>=i&&(s++,n.push("")),n[s]=`${n[s]}${""===n[s]?"":" "}${e}`}),Object.assign({...e},{text_lines:n})}))}get_total_height(e){return(e=e||this.rows).reduce((e,t)=>e+t.text_height+t.padding_height,0)+this.container_padding_height}to_scaled(e){return new t({...this.params,scale_factor:e})}get_minimum_panel_height_for_scene(e){var e=2<(t=this.scenes_formatted_choices.slice(e,e+1))[0].length?2:1,t=this.get_rows_metrics(e,t);return this.get_total_height(t)}get_one_choices_row_metrics(e,t){return e=this.scenes_formatted_choices.slice(e,e+1),t=this.get_rows_metrics(t+1,e)[t],Object.assign(t,{text_height:t.text_height,padding_height:t.padding_height})}}},{"../lib/font-tools":7}],23:[function(e,t,n){"use strict";const s=e("../lib/frame-rate-controller"),{get_optimal_visible_foreground_color:u}=e("../lib/color-tools"),{get_canvas_char_size:p}=e("../lib/font-tools"),i=e("../model/scene-types"),f=e("./ui-components/choice-cpt"),o=e("./ui-components/choices-panel-cpt"),b=e("./ui-components/closing-icon-cpt"),a=e("./ui-components/game-object-cpt"),r=e("./ui-components/inventory-cpt"),v=e("./ui-components/inventory-object-cpt"),w=e("./ui-components/inventory-slot-cpt"),c=e("./ui-components/scene-animation-cpt"),x=e("./ui-components/text-box-cpt"),y=e("./ui-components/user-error-popup"),l=e("./canvas-dimensions");t.exports=class{constructor(e){this.params=e;e=this.params.frame_rate||30;this.fps_controller=new s(e),this.params.container.style.backgroundColor=this.params.get_game_settings("general").background_color,this.canvas=document.createElement("canvas"),this.canvas.style.backgroundColor="black",window.mentalo_drawing_context=this.canvas.getContext("2d"),this.canvas_dimensions={},this.canvas_zones={},this.event_listeners={game_objects:[],inventory:[],text_box:[]},this.state={user_error_popup:{is_set:!1,text:"",on_close:function(){}}},this.loading_frame=0}init(){this.params.fullscreen&&this.set_full_screen(),this.set_canvas_dimensions(),this.create_canvas_zones(),this.params.container.appendChild(this.canvas);const e=window.mentalo_drawing_context;e.mozImageSmoothingEnabled=!1,e.webkitImageSmoothingEnabled=!1,e.msImageSmoothingEnabled=!1,e.imageSmoothingEnabled=!1,this.create_components()}clear_event_listeners(){Object.values(this.components).forEach(e=>{e.clear_event_listeners()})}clear_children(t={exclude:[]}){Object.values(this.components).forEach(e=>{e.clear_children(t)})}reset_text_box_visibility(){this.components.scene_animation.set_text_box_visibility(!0)}set_full_screen(){const e=document.body;e.requestFullScreen=e.requestFullScreen||e.webkitRequestFullScreen||e.msRequestFullscreen||e.mozRequestFullScreen;try{e.requestFullScreen()}catch(e){console.error(e.message)}}exit_fullscreen(){document.fullscreenElement&&document.exitFullscreen()}set_user_error_popup(e){this.state.user_error_popup={is_set:!0,text:e.text,stream_text:!!e.stream_text,on_close:function(){e.on_close&&e.on_close()}},this.clear_event_listeners(),this.clear_children({exclude:["TextBoxCpt","InventoryCpt","ChoicesPanelCpt","GameObjectCpt"]})}clear_user_error_popup(){this.reset_user_error_popup(),this.clear_event_listeners(),this.clear_children({exclude:["TextBoxCpt","InventoryCpt","ChoicesPanelCpt","GameObjectCpt"]})}reset_user_error_popup(){this.state.user_error_popup={is_set:!1,text:"",on_close:function(){}}}on_close_user_error_popup(){this.state.user_error_popup.on_close(),this.clear_user_error_popup()}set_canvas_dimensions(){var{get_game_settings:e,get_game_scenes:t}=this.params;this.canvas_dimensions=new l({get_game_settings:e,get_game_scenes:t}),this.canvas.width=this.canvas_dimensions.width,this.canvas.height=this.canvas_dimensions.height}get_scaled_choices_panel_settings(){return this.canvas_dimensions.choices_panel_metrics.settings}get_scaled_inventory_settings(){var{scale_factor:e}=this.canvas_dimensions;const{get_game_settings:t}=this.params;var n=t("inventory");return Object.assign({...n},{slot_rounded_corner_radius:n.slot_rounded_corner_radius*e,slot_border_width:n.slot_border_width*e,gap:n.gap*e,padding:n.padding*e})}get_scaled_text_boxes_settings(){var{scale_factor:e}=this.canvas_dimensions;const{get_game_settings:t}=this.params;var n=t("text_boxes");return Object.assign({...n},{font_size:n.font_size*e,padding:n.padding*e,margin:n.margin*e,rounded_corners_radius:n.rounded_corners_radius*e,border_width:n.border_width*e})}create_canvas_zones(){var e=this.canvas_dimensions,t=this.get_scaled_inventory_settings(),n=this.get_scaled_choices_panel_settings();this.canvas_zones={root:{left:0,top:0,right:e.width,bottom:e.height,width:e.width,height:e.height,padding:0},scene_animation:{left:0,top:0,right:e.image.width,bottom:e.image.height,width:e.image.width,height:e.image.height,clear_color:"black",padding:0},inventory:{left:e.image.width,top:0,right:e.width,bottom:e.inventory.height,width:e.width-e.image.width,height:e.inventory.height,clear_color:t.background_color,padding:t.padding},choices_panel:{left:0,top:e.image.height,right:e.width,bottom:e.height,width:e.width,height:e.height-e.image.height,clear_color:n.background_color,padding:n.container_padding}}}scene_is_not_cinematic(){return this.params.get_scene()._type===i.PLAYABLE}create_components(){const{get_game_settings:e,get_inventory:d,get_scene:m,get_game_scenes:l,get_scene_index:_}=this.params,{scale_factor:h}=this.canvas_dimensions,g=this.scene_is_not_cinematic.bind(this);this.components={scene_animation:new c({bounding_zone:this.canvas_zones.scene_animation,get_animation:()=>m().animation,next_frame_ready:()=>this.fps_controller.nextFrameReady(),get_children:()=>{const e=m();return e.game_objects.map(t=>{var e=this.canvas_zones.scene_animation,n={x:t.position.x*h+e.left,y:t.position.y*h+e.top},e={w:t.image.width*h,h:t.image.height*h};const s={left:n.x,right:n.x+e.w,top:n.y,bottom:n.y+e.h,width:e.w,height:e.h};e={bounding_zone:s,position:n,dimensions:e,image:t.image,is_in_inventory:()=>d().has(t)};const i=new a(e);return i.add_event_listener({event_type:"mousemove",listener:e=>{d().has(t)||(e=e.offsetX>=s.left&&e.offsetX<=s.right&&e.offsetY>=s.top&&e.offsetY<=s.bottom,i.state.draw_border=e)}}),i.add_event_listener({event_type:"click",listener:e=>{!d().has(t)&&e.offsetX>=s.left&&e.offsetX<=s.right&&e.offsetY>=s.top&&e.offsetY<=s.bottom&&(this.params.on_game_object_click(t),this.clear_event_listeners(),this.clear_children({exclude:"TextBoxCpt"}))}}),i}).concat(e.text_box?[(()=>{const e=this.get_scaled_text_boxes_settings();var t,n,s,n=(t=this.canvas_zones.scene_animation,o=e.padding,i=e.margin,n=t.left+i,s=t.right-i,i=t.bottom-i,{left:n,right:s,top:o=i-2*o,bottom:i,width:s-n,height:i-o}),i=10*h,o={x:n.right-i/2,y:n.top+i/2};const a={color:e.font_color,radius:i,center:o,background_color:e.background_color,line_width:Math.floor(2*h),bounding_zone:{left:o.x-i,right:o.x+i,top:o.y-i,bottom:o.y+i,width:2*i,height:2*i}},r=new x({text:m().text_box,settings:e,bounding_zone:n,get_visibility_state:()=>this.components.scene_animation.state.text_box_visible,get_children:()=>[new b(a)]}),c=r.children[0];return r.children[0].add_event_listener({event_type:"click",listener:e=>{var t=c.params.bounding_zone,t=e.offsetX>=t.left&&e.offsetX<=t.right&&e.offsetY>=t.top&&e.offsetY<=t.bottom;const{scene_animation:n}=this.components;t&&n.state.text_box_visible&&n.set_text_box_visibility(!1)}}),r})()]:[]).concat(this.state.user_error_popup.is_set?[(()=>{const{text:e,stream_text:t}=this.state.user_error_popup;var n=this.get_scaled_text_boxes_settings(),s=this.canvas_zones.scene_animation,i=p(window.mentalo_drawing_context,n),o=s.width/2,a=2*i.width;const r=(o-2*a)/i.width,c=[""];let l=0;e.split(" ").forEach(e=>{`${c[l]}${e} `.length>r&&(l++,c.push("")),c[l]+=`${e} `});var _=i.text_line_height,i=c.length*_+2*a-i.interline_height;const h=n.background_color,d=n.font_color,m={left:s.left+s.width/2-o/2,top:s.top+s.height/2-i/2,right:s.left+s.width/2+o/2,bottom:s.top+s.height/2-i/2+i,width:o,height:i,clear_color:h};a={settings:n,bounding_zone:m,modal_bounds:this.canvas_zones.scene_animation,clear_modal_color:"#0004",text_lines:c,padding:a,stream_text:t,get_children:()=>{var e={x:m.right-5,y:m.top+5};const t={left:e.x-15,right:e.x+15,top:e.y-15,bottom:e.y+15,width:30,height:30};e={color:d,radius:15,center:e,background_color:h,line_width:2,bounding_zone:t};const n=new b(e);return n.add_event_listener({event_type:"click",listener:e=>{e.offsetX>=t.left&&e.offsetX<=t.right&&e.offsetY>=t.top&&e.offsetY<=t.bottom&&this.on_close_user_error_popup()}}),[n]}};return new y(a)})()]:[])}}),inventory:new r({bounding_zone:this.canvas_zones.inventory,get_inventory:d,is_visible:g,invisible_clear_color:e("general").background_color,get_children:()=>{const i=this.get_scaled_inventory_settings(),a={left:this.canvas_zones.inventory.left+i.padding,top:this.canvas_zones.inventory.top+i.padding,right:this.canvas_zones.inventory.right-i.padding,bottom:this.canvas_zones.inventory.bottom-i.padding,width:this.canvas_zones.inventory.width-2*i.padding,height:this.canvas_zones.inventory.height-2*i.padding},r=i.gap;const c=(a.height-(i.rows-1)*r)/i.rows;var e=c*i.columns+r*(i.columns-1);const l=(a.width-e)/2,_=[];let h=0;return Array.from({length:i.rows}).forEach((e,t)=>{const s=a.top+t*(c+r);Array.from({length:i.columns}).forEach((e,t)=>{t=a.left+t*(c+r)+l;const o={left:t,top:s,right:t+c,bottom:s+c,width:c,height:c},n=u(i.background_color);t={bounding_zone:o,stroke_color:n,is_visible:g,settings:i,get_children:()=>{const s=new v({slot_index:h,is_visible:g,settings:i,stroke_color:n,get_game_object:e=>{const t=Array.from(d())[e];if(t){var n=t.get_dimensions(),e=n.width/n.height;const s={width:0,height:0},i={x:0,y:0};return 1<e?(s.width=c,s.height=c*(n.height/n.width),i.x=o.left,i.y=o.top+(c/2-s.height/2)):e<1?(s.height=c,s.width=c*e,i.y=o.top,i.x=o.left+(c/2-s.width/2)):(s.width=c,s.height=c,i.x=o.left,i.y=o.top),{ref:t,position:i,dimensions:s}}},bounding_zone:o,get_children:()=>[new b({is_visible:g,color:"#bf5e43",center:{x:o.left+c/2,y:o.top+c/2},radius:c/4,line_width:2<c/20?c/20:2,bounding_zone:{...o,clear_color:"rgba(0,0,0,0.2)"}})]});return s.add_event_listener({event_type:"mousemove",listener:e=>{var t=s.params.get_game_object(s.params.slot_index),n=s.params.bounding_zone;s.state.draw_inventory_close_icon=s.params.is_visible()&&!!t&&m().game_objects.includes(t.ref)&&e.offsetX>=n.left&&e.offsetX<=n.right&&e.offsetY>=n.top&&e.offsetY<=n.bottom}}),s.children[0].add_event_listener({event_type:"click",listener:e=>{if(!s.params.is_visible())return!1;var t=s.params.get_game_object(s.params.slot_index),n=s.params.bounding_zone,n=e.offsetX>=n.left&&e.offsetX<=n.right&&e.offsetY>=n.top&&e.offsetY<=n.bottom;t&&n&&m().game_objects.includes(t.ref)&&(this.params.on_drop_inventory_object(t.ref),this.clear_event_listeners(),this.clear_children({exclude:"TextBoxCpt"}))}}),[s]}},t=new w(t);_.push(t),h++})}),_}}),choices_panel:new o({bounding_zone:this.canvas_zones.choices_panel,minimum_bounding_zone:()=>{var e=this.canvas_zones.choices_panel,t=this.canvas_dimensions.choices_panel_metrics.get_minimum_panel_height_for_scene(_()),t=this.canvas_dimensions.choices_panel_metrics.get_total_height()-t;return Object.assign({...e},{bottom:e.bottom-t,height:e.height-t})},is_visible:g,invisible_clear_color:e("general").background_color,get_children:()=>Array.from({length:2*this.canvas_dimensions.choices_panel_metrics.get_max_choices_row_per_scene(_())}).map((e,n)=>{var t=this.get_scaled_choices_panel_settings();const s={left:this.canvas_zones.choices_panel.left+t.container_padding,right:this.canvas_zones.choices_panel.right-t.container_padding,top:this.canvas_zones.choices_panel.top+t.container_padding,bottom:this.canvas_zones.choices_panel.bottom-t.container_padding,width:0,height:0};s.width=s.right-s.left,s.height=s.bottom-s.top;var i=s.width/2;const{choices_panel_metrics:o}=this.canvas_dimensions;var a=o.rows.map(e=>e.text_height+e.padding_height);const r={left:s.left+n%2*i,top:s.top+(1<n?1:0)*a[0],right:s.left+n%2*i+i,bottom:s.top+(1<n?1:0)*a[1]+a[1<n?1:0],width:i,height:a[1<n?1:0]},c=new f({is_visible:g,minimum_bounding_zone:()=>{var e,t;return c.minimum_bounding_zone||(e=1<n?1:0,e=(t=this.canvas_dimensions.choices_panel_metrics.get_one_choices_row_metrics(_(),e)).text_height+t.padding_height,t=(()=>{const e=this.components.choices_panel.children;return 2<=e.indexOf(c)?r.top-e[0].params.minimum_bounding_zone().bottom:0})(),c.minimum_bounding_zone=Object.assign({...r},{height:e,top:r.top-t,bottom:r.top-t+e})),c.minimum_bounding_zone},get_formatted_choice:()=>{var e=this.canvas_dimensions.choices_panel_metrics.scenes_formatted_choices[l().indexOf(m())];return e.length-1>=n?e[n]:void 0},settings:t});return c.add_event_listener({event_type:"mousemove",listener:e=>{c.state.active=c.params.is_visible()&&!!c.params.get_formatted_choice()&&c.is_hover(e)}}),c.add_event_listener({event_type:"click",listener:()=>{var e;c.state.active&&(e=c.params.get_formatted_choice(),this.params.on_choice_click(e)&&(this.clear_event_listeners(),this.clear_children()))}}),c})})}}draw_loading(){const e=window.mentalo_drawing_context;e.save(),e.font="25px monospace",e.fillStyle="black",e.fillRect(0,0,window.innerWidth,window.innerHeight),e.fillStyle="white",e.fillText("Loading",50,window.innerHeight/2);var t=Array.from({length:++this.loading_frame}).map(()=>".").join("");e.font="8px monospace",e.fillText(t,50,window.innerHeight/2+20),e.restore()}draw_game(){Object.values(this.components).forEach(e=>e.draw())}}},{"../lib/color-tools":6,"../lib/font-tools":7,"../lib/frame-rate-controller":8,"../model/scene-types":18,"./canvas-dimensions":21,"./ui-components/choice-cpt":24,"./ui-components/choices-panel-cpt":25,"./ui-components/closing-icon-cpt":26,"./ui-components/game-object-cpt":27,"./ui-components/inventory-cpt":28,"./ui-components/inventory-object-cpt":29,"./ui-components/inventory-slot-cpt":30,"./ui-components/scene-animation-cpt":31,"./ui-components/text-box-cpt":32,"./ui-components/user-error-popup":34}],24:[function(e,t,n){"use strict";const{draw_text_in_bounds:l}=e("../../lib/text-tools"),{draw_rect:_}=e("../../lib/shape-tools");class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("ChoiceCpt")}draw(){super.draw();const{get_formatted_choice:e,settings:t}=this.params;var n,s,i,o=this.params.minimum_bounding_zone(),a={left:o.left+t.choice_padding,right:o.right-t.choice_padding,top:o.top+t.choice_padding,bottom:o.bottom-t.choice_padding,width:o.width-2*t.choice_padding,height:o.height-2*t.choice_padding},r=e(),c=window.mentalo_drawing_context;r&&(this.state.active&&({left:n,top:s,width:i,height:o}=o,_(c,n,s,i,o,{fill_color:t.active_choice_background_color,rounded_corners_radius:t.active_choice_rounded_corners_radius,border:{width:t.active_choice_border_width,color:t.font_color}})),l(c,r.text_lines,a,t,{},!1))}is_hover(e){var t=this.params.minimum_bounding_zone();return e.offsetX>=t.left&&e.offsetX<=t.right&&e.offsetY>=t.top&&e.offsetY<=t.bottom}}t.exports=s},{"../../lib/shape-tools":9,"../../lib/text-tools":10,"./ui-component":33}],25:[function(e,t,n){"use strict";class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("ChoicesPanelCpt")}draw(){super.draw(),this.params.is_visible()?(this.clear_bounding_zone({clear_color:this.params.invisible_clear_color}),this.clear_bounding_zone({use_bounding_zone:this.params.minimum_bounding_zone()}),this.draw_children()):this.clear_bounding_zone({clear_color:this.params.invisible_clear_color})}}t.exports=s},{"./ui-component":33}],26:[function(e,t,n){"use strict";class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("ClosingIconCpt")}draw(){super.draw(),this.clear_bounding_zone();var{color:e,center:t,radius:n=5,line_width:s=2,background_color:i="rgba(0,0,0,0)"}=this.params;const o=window.mentalo_drawing_context;var a=n/1.5,a={left:t.x-n+a,right:t.x+n-a,top:t.y-n+a,bottom:t.y+n-a};o.fillStyle=i,o.beginPath(),o.arc(t.x,t.y,n,0,2*Math.PI),o.fill(),o.strokeStyle=e,o.lineWidth=s,o.beginPath(),o.arc(t.x,t.y,n,0,2*Math.PI),o.stroke(),o.beginPath(),o.moveTo(a.left,a.top),o.lineTo(a.right,a.bottom),o.moveTo(a.left,a.bottom),o.lineTo(a.right,a.top),o.stroke()}}t.exports=s},{"./ui-component":33}],27:[function(e,t,n){"use strict";class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("GameObjectCpt")}draw(){super.draw();const{position:e,dimensions:t,image:n,is_in_inventory:s}=this.params;if(!s()){const i=window.mentalo_drawing_context;i.drawImage(n,0,0,n.width,n.height,e.x,e.y,t.w,t.h),this.state.draw_border&&(i.lineWidth=1,i.strokeStyle="rgba(180, 180, 180, 0.5)",i.strokeRect(e.x-5,e.y-5,t.w+10,t.h+10))}}}t.exports=s},{"./ui-component":33}],28:[function(e,t,n){"use strict";class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("InventoryCpt")}draw(){super.draw(),this.params.is_visible()?(this.clear_bounding_zone(),this.draw_children()):this.clear_bounding_zone({clear_color:this.params.invisible_clear_color})}}t.exports=s},{"./ui-component":33}],29:[function(e,t,n){"use strict";const{draw_rect:c}=e("../../lib/shape-tools");class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("InventoryObjectCpt")}draw(){super.draw();const{get_game_object:e,slot_index:t,settings:n,bounding_zone:s,stroke_color:i}=this.params;var o,a,r=e(t);r&&(o=window.mentalo_drawing_context,a=r.ref.image,c(o,s.left,s.top,s.width,s.height,{fill_image:{src:a,dw:r.dimensions.width,dh:r.dimensions.height,dx:r.position.x,dy:r.position.y},rounded_corners_radius:n.slot_rounded_corner_radius,border:{width:n.slot_border_width,color:i}}),this.state.draw_inventory_close_icon&&this.draw_children())}}t.exports=s},{"../../lib/shape-tools":9,"./ui-component":33}],30:[function(e,t,n){"use strict";const{draw_rect:r}=e("../../lib/shape-tools");class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("InventorySlotCpt")}draw(){super.draw();var{bounding_zone:e,stroke_color:t,settings:n}=this.params,{left:s,top:i,width:o,height:a}=e,e=window.mentalo_drawing_context;r(e,s,i,o,a,{fill_color:"rgba(0,0,0,0)",rounded_corners_radius:n.slot_rounded_corner_radius,border:{width:n.slot_border_width,color:t}}),this.draw_children()}}t.exports=s},{"../../lib/shape-tools":9,"./ui-component":33}],31:[function(e,t,n){"use strict";class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("SceneAnimationCpt"),this.framecount=0,this.state.text_box_visible=!0}set_text_box_visibility(e){this.state.text_box_visible=e}update_framecount(){this.framecount=this.framecount+1<=Number.MAX_SAFE_INTEGER?this.framecount+1:0}draw(){super.draw();const{bounding_zone:e,get_animation:t,next_frame_ready:n}=this.params;if(n()){this.clear_bounding_zone();const _=window.mentalo_drawing_context,h=t();h.update_frame(this.framecount);var s=h.dimensions,i=s.width,o=s.height,a=h.frame*i,r=e.width,c=e.height;h.canvas_precalc||(h.canvas_precalc={dx:e.left,dy:e.top,dw:r,dh:c},r/c<i/o?(h.canvas_precalc.dw=r,h.canvas_precalc.dh=r*(o/i),h.canvas_precalc.dy=e.top+(c-h.canvas_precalc.dh)/2):i/o<r/c?(h.canvas_precalc.dh=c,h.canvas_precalc.dw=c*(i/o),h.canvas_precalc.dx=e.left+(r-h.canvas_precalc.dw)/2):(h.canvas_precalc.dh=c,h.canvas_precalc.dw=r));var{dx:l,dy:s,dw:c,dh:r}=h.canvas_precalc;_.drawImage(h.image,a,0,i,o,l,s,c,r),this.draw_children(),this.update_framecount()}}}t.exports=s},{"./ui-component":33}],32:[function(e,t,n){"use strict";const{draw_text_in_bounds:r}=e("../../lib/text-tools"),{get_canvas_char_size:_}=e("../../lib/font-tools"),{draw_rect:c}=e("../../lib/shape-tools");class s extends e("./ui-component"){constructor(e){super(e),this.set_str_id("TextBoxCpt"),this.state.text={}}init(){const{text:e,settings:t,bounding_zone:n}=this.params;var s=_(window.mentalo_drawing_context,t),i=s.text_line_height,o=3*s.width;const a=(n.width-o)/s.width,r=[],c=e.split("\n");c.forEach(e=>{const t=[""];let n=0;for(const s of e.split(" "))(t[n]+s+" ").length>a&&(t.push(""),n++),t[n]+=s+" ";t.forEach(e=>r.push(e))});o=r.length*i-s.interline_height,s=Object.assign({...n},{height:n.height+o,top:n.top-o});this.state.text={lines:r,line_height:i,bounding_zone:s};const l=this.children[0];s=l.params.bounding_zone,Object.assign(s,{top:s.top-o,bottom:s.bottom-o});l.params.center.y-=o,this.state.initialized=!0}draw(){var e,t,n,s,i,o,a;this.params.get_visibility_state()&&(super.draw(),{settings:e}=this.params,this.state.initialized||this.init(),t=window.mentalo_drawing_context,{lines:n,bounding_zone:s}=this.state.text,i={x:s.left,y:s.top},o=s.width,a=s.height,c(t,i.x,i.y,o,a,{fill_color:e.background_color,rounded_corners_radius:e.rounded_corners_radius,border:{width:e.border_width,color:e.font_color}}),r(t,n,s,e,this.state.text,!0)&&this.draw_children())}}t.exports=s},{"../../lib/font-tools":7,"../../lib/shape-tools":9,"../../lib/text-tools":10,"./ui-component":33}],33:[function(e,t,n){"use strict";const{draw_rect:a}=e("../../lib/shape-tools");t.exports=class{constructor(e){this.set_str_id(),this.params=e,this.params.event_listeners=this.params.event_listeners||[],this.params.get_children=this.params.get_children||(()=>[]),this.params.is_visible=this.params.is_visible||(()=>!0),this.children=[],this.children_set=!1,this.set_children(),this.state={event_listeners_initialized:!1},this.init_event_listeners()}set_str_id(e=this.constructor.name){this.str_id=e}clear_bounding_zone(e={}){let{bounding_zone:t}=this.params;var{use_bounding_zone:n}=e;t=n||t;var{left:s,top:i,width:o,height:n}=t;a(window.mentalo_drawing_context,s,i,o,n,{fill_color:e.clear_color||t.clear_color||"rgba(0,0,0,0)"})}clear_event_listeners(e={recursive:!0}){this.params.event_listeners.forEach(e=>{window.removeEventListener(e.event_type,e.listener)}),e.recursive&&this.children.forEach(e=>{e.clear_event_listeners()}),this.state.event_listeners_initialized=!1}clear_children(e){const{exclude:t}=e,n=[];this.children.forEach(e=>{t.includes(e.str_id)&&n.push(e)}),this.children=n,this.children_set=!1}set_children(){this.children=this.children.concat(this.params.get_children().filter(t=>{return!this.children.find(e=>e.str_id===t.str_id)})),this.children_set=!0}add_event_listener(e){e=this.params.event_listeners.push(e),e=this.params.event_listeners[e-1];window.addEventListener(e.event_type,e.listener)}init_event_listeners(){const{event_listeners:e}=this.params;e.forEach(e=>this.add_event_listener(e)),this.state.event_listeners_initialized=!0}draw_children(){this.children_set||this.set_children(),this.children.forEach(e=>e.draw())}draw(){this.state.event_listeners_initialized||this.init_event_listeners()}}},{"../../lib/shape-tools":9}],34:[function(e,t,n){"use strict";const{draw_text_in_bounds:l}=e("../../lib/text-tools"),{draw_rect:_}=e("../../lib/shape-tools");class s extends e("./ui-component"){constructor(e){super(e),this.state={text:{}},this.set_str_id("UserErrorPopup")}draw(){super.draw();var{text_lines:e,bounding_zone:t,padding:n,settings:s,modal_bounds:i,clear_modal_color:o}=this.params;this.clear_bounding_zone({use_bounding_zone:i,clear_color:o});var a=window.mentalo_drawing_context,{left:r,top:c,width:i,height:o}=t;_(a,r,c,i,o,{fill_color:s.background_color,rounded_corners_radius:s.rounded_corners_radius,border:{width:s.border_width,color:s.font_color}}),l(a,e,t,Object.assign({...s},n),this.state.text,!!this.params.stream_text)&&this.draw_children()}}t.exports=s},{"../../lib/shape-tools":9,"../../lib/text-tools":10,"./ui-component":33}],35:[function(e,t,n){const s=["en","fr","es"],i={"Some scenes have an empty image, game cannot be loaded":{fr:"Certaines scènes ont une image vide, le jeu ne peut pas être chargé",es:"Algunas escenas tienen una imagen vacÃa, el juego no se puede cargar"},"Destination scene has not been set.":{fr:"La scène de destination n'a pas été définie.",es:"La escena de destino no ha sido definida."},"Next scene has not been set.":{fr:"La scène suivante n'a pas été définie",es:"La siguiente escena no ha sido definida."}};t.exports={get_translated:function(e,t){return i[e]&&"en"!==t&&s.includes(t)?i[e][t]:e},supported_locales:s}},{}],36:[function(e,t,n){"use strict";t.exports={register_key:"objectToHtmlRender",register(e){e=e||this.register_key;window[e]=this},setRenderCycleRoot(e){this.renderCycleRoot=e},event_name:"objtohtml-render-cycle",setEventName(e){this.event_name=e},objectToHtml(t){if(!t)return document.createElement("span");const n=this.objectToHtml.bind(this),{tag:e,xmlns:s}=t,i=void 0!==s?document.createElementNS(s,e):document.createElement(e),o=["tag","contents","style_rules","state","xmlns"];return Object.keys(t).filter(e=>!o.includes(e)).forEach(e=>{switch(e){case"class":i.classList.add(...t[e].split(" ").filter(e=>""!==e));break;case"on_render":t.id||(i.id=`${btoa(JSON.stringify(t).slice(0,127)).replace(/\=/g,"")}${window.performance.now()}`),"function"!=typeof t.on_render?console.error("The on_render attribute must be a function"):this.attach_on_render_callback(i,t.on_render);break;default:void 0!==s?i.setAttributeNS(null,e,t[e]):i[e]=t[e]}}),t.contents&&"string"==typeof t.contents?i.innerHTML=t.contents:t.contents&&0<t.contents.length&&t.contents.forEach(e=>{switch(typeof e){case"string":i.innerHTML=e;break;case"object":void 0!==s&&(e=Object.assign(e,{xmlns:s})),i.appendChild(n(e))}}),t.style_rules&&Object.keys(t.style_rules).forEach(e=>{i.style[e]=t.style_rules[e]}),i},on_render_callbacks:[],attach_on_render_callback(t,n){var e={callback:e=>{e.detail.outputNode!==t&&!e.detail.outputNode.querySelector(`#${t.id}`)||(n(t),-1===(e=this.on_render_callbacks.indexOf(this.on_render_callbacks.find(e=>e.node===t)))?console.warn("A callback was registered for node with id "+t.id+" but callbacck handler is undefined."):(window.removeEventListener(this.event_name,this.on_render_callbacks[e].callback),this.on_render_callbacks.splice(e,1)))},node:t},e=this.on_render_callbacks.push(e);window.addEventListener(this.event_name,this.on_render_callbacks[e-1].callback)},renderCycle:function(){var e,e=document.getElementsByTagName("main")[0]||(e=document.createElement("main"),document.body.appendChild(e),e);this.subRender(this.renderCycleRoot.render(),e,{mode:"replace"})},subRender(e,t,n={mode:"append"}){let s=null;var i=()=>(s=this.objectToHtml(e),s);switch(n.mode){case"append":t.appendChild(i());break;case"override":t.innerHTML="",t.appendChild(i());break;case"insert-before":t.insertBefore(i(),t.childNodes[n.insertIndex]);break;case"adjacent":t.insertAdjacentHTML(n.insertLocation,i());break;case"replace":t.parentNode.replaceChild(i(),t);break;case"remove":t.remove()}var o=this.event_name,o=new CustomEvent(o,{detail:{inputObject:e,outputNode:s,insertOptions:n,targetNode:t}});window.dispatchEvent(o)}}},{}],37:[function(e,t,n){"use strict";t.exports=class{constructor(e){this.props=e,this.id=this.props.images.join("").replace(/\s\./g),this.state={showImageIndex:0},this.RUN_INTERVAL=5e3,1<this.props.images.length&&this.run()}run(){this.runningInterval=setInterval(()=>{var{showImageIndex:e}=this.state,{images:t}=this.props;this.state.showImageIndex=e<t.length-1?++e:0,this.refreshImage()},this.RUN_INTERVAL)}setImageIndex(e){clearInterval(this.runningInterval),this.state.showImageIndex=e,this.refreshImage()}refreshImage(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}render(){const{showImageIndex:n}=this.state,{images:e}=this.props;return{tag:"div",id:this.id,class:"image-carousel",contents:[{tag:"img",property:"image",alt:`image carousel ${e[n].replace(/\.[A-Za-z]+/,"")}`,src:e[n]},1<e.length&&{tag:"div",class:"carousel-bullets",contents:e.map((e,t)=>{return{tag:"span",class:`bullet ${n===t?"active":""}`,onclick:this.setImageIndex.bind(this,t)}})}]}}}},{}],38:[function(e,t,n){"use strict";const{fetch_json_or_error_text:s}=e("./fetch");t.exports={loadArticles:function(e,t){return s(`/articles/${e}/${t}`)},getArticleBody:function(e){return e.replaceAll("\n","<br/>")},getArticleDate:function(e){return`${e.getDate()}-${e.getMonth()+1}-${e.getFullYear()}`}}},{"./fetch":39}],39:[function(e,t,n){"use strict";t.exports={fetchjson:function(e){return new Promise((t,n)=>{fetch(e).then(e=>e.json()).then(e=>t(e)).catch(e=>n(e))})},fetchtext:function(e){return new Promise((t,n)=>{fetch(e).then(e=>e.text()).then(e=>t(e)).catch(e=>n(e))})},fetch_json_or_error_text:async function(e,s={}){return new Promise((t,n)=>{fetch(e,s).then(async e=>{400<=e.status&&e.status<600?n(await e.text()):t(await e.json())})})}}},{}],40:[function(e,t,n){"use strict";const s=e("ks-cheap-translator"),{translations_url:i}=e("../../constants");t.exports=class{constructor(e){Object.assign(this,e),this.id||(this.id="webpage-"+performance.now()),s.init({translations_url:i,supported_languages:["fr","en"]}).then(this.refresh_all.bind(this))}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}refresh_all(){obj2htm.renderCycle()}}},{"../../constants":3,"ks-cheap-translator":4}],41:[function(e,t,n){"use strict";const{images_url:i}=e("../../../../../admin-frontend/src/constants"),{data_url:s}=e("../../../../constants"),o=e("../../../generic-components/image-carousel"),{getArticleBody:a}=e("../../../lib/article-utils"),{fetch_json_or_error_text:r}=e("../../../lib/fetch"),{MentaloEngine:c}=e("mentalo-engine"),l=e("ks-cheap-translator"),_=l.trad.bind(l);t.exports=class{constructor(e){this.props=e,this.parse_body()}parse_body(){let e=a(this.props.body);var t=/\[PLAY_BUTTON\s\{.+\}\]/g;const n=e.match(t);n&&(this.build_play_button(JSON.parse(n[0].replace(/[\[\]PLAY_BUTTON\s]/g,""))),e=e.replace(t,"")),this.body=e}build_play_button(e){this.render_play_button={tag:"button",class:"play-button",contents:_("Jouer"),onclick:this.handle_click_play.bind(this,e.filename,e.engine)}}load_and_run_mentalo_game(e,t){const n=t.innerHTML;t.innerHTML="Loading ...",t.style.pointerEvents="none",r(`${s}/${e}`).then(e=>{const t=document.createElement("div");t.style.position="fixed",t.style.top=0,t.style.left=0,t.style.right=0,t.style.bottom=0,t.style.zIndex=10,t.style.display="flex",t.style.justifyContent="center",t.style.alignItems="center",t.id="kuadrado-tmp-game-player-container",document.body.appendChild(t),document.body.style.overflow="hidden";const n=new c({game_data:e,fullscreen:!0,frame_rate:30,container:t,on_quit_game:()=>{t.remove(),document.body.style.overflow="visible"}});n.init(),n.run_game()}).catch(e=>console.log(e)).finally(()=>{t.innerHTML=n,t.style.pointerEvents="unset"})}handle_click_play(e,t,n){"mentalo"===t?this.load_and_run_mentalo_game(e,n.target):console.log("Error, unkown engine")}render(){const{title:e,subtitle:t,images:n,details:s}=this.props;return{tag:"article",typeof:"VideoGame",additionalType:"Article",class:"game-article",contents:[{tag:"h2",property:"name",class:"game-title",contents:e},{tag:"div",class:"game-banner",contents:[{tag:"img",class:"pixelated",src:`${i}/${n[0]}`}]},{tag:"h3",class:"game-subtitle",contents:t,property:"alternativeHeadline"},{tag:"div",class:"game-description",property:"description",contents:[{tag:"p",style_rules:{margin:0},contents:this.body}].concat(this.render_play_button?[this.render_play_button]:[])},new o({images:n.map(e=>`${i}/${e}`)}).render(),0<s.length&&{tag:"div",class:"article-details",contents:[{tag:"h2",contents:"Details"},{tag:"ul",class:"details-list",contents:s.map(e=>({tag:"li",class:"detail",contents:[{tag:"label",contents:e.label},{tag:"div",class:"detail-value",contents:e.value}]}))}]}]}}}},{"../../../../../admin-frontend/src/constants":1,"../../../../constants":3,"../../../generic-components/image-carousel":37,"../../../lib/article-utils":38,"../../../lib/fetch":39,"ks-cheap-translator":4,"mentalo-engine":5}],42:[function(e,t,n){"use strict";const{loadArticles:s}=e("../../../lib/article-utils"),i=e("./game-article"),o=e("ks-cheap-translator");t.exports=class{constructor(e){this.props=e,this.state={articles:[]},this.id="game-articles-section",this.loadArticles()}loadArticles(){s("games",o.locale).then(e=>{this.state.articles=e,this.refresh()}).catch(e=>console.log(e))}renderPlaceholder(){return{tag:"article",class:"placeholder",contents:[{tag:"div"},{tag:"div"}]}}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}render(){const{articles:e}=this.state;return{tag:"section",class:"game-articles page-contents-center",id:this.id,contents:0<e.length?e.map(e=>new i({...e}).render()):[this.renderPlaceholder()]}}}},{"../../../lib/article-utils":38,"./game-article":41,"ks-cheap-translator":4}],43:[function(e,t,n){"use strict";const{images_url:s}=e("../../../constants");var i=e("../../lib/web-page");const o=e("./components/game-articles"),a=e("ks-cheap-translator"),r=a.trad.bind(a);class c extends i{render(){return{tag:"div",id:"games-page",contents:[{tag:"div",class:"page-header logo-left",contents:[{tag:"div",class:"page-contents-center grid-wrapper",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"image game controller",src:`${s}/game_controller.svg`}]},{tag:"h1",contents:r("Jeux")},{tag:"p",contents:r("games-page-intro")}]}]},(new o).render()]}}}t.exports=c},{"../../../constants":3,"../../lib/web-page":40,"./components/game-articles":42,"ks-cheap-translator":4}],44:[function(e,t,n){"use strict";const s=e("../../run-page");e=e("./games");s(e)},{"../../run-page":45,"./games":43}],45:[function(e,t,n){"use strict";const s=e("object-to-html-renderer"),i=e("./template/template");t.exports=function(e){e=new i({page:new e});s.register("obj2htm"),obj2htm.setRenderCycleRoot(e),obj2htm.renderCycle()}},{"./template/template":47,"object-to-html-renderer":36}],46:[function(e,t,n){"use strict";const{images_url:s}=e("../../../constants"),a=e("ks-cheap-translator"),r=a.trad.bind(a),i=[{url:"/games/",text:"Jeux"},{url:"/education/",text:"Pédagogie"},{url:"/software-development/",text:"Software"}];t.exports=class{constructor(){this.initEventHandlers()}handleBurgerClick(){document.getElementById("nav-menu-list").classList.toggle("responsive-show")}initEventHandlers(){window.addEventListener("click",e=>{"nav-menu-list"===e.target.id||e.target.classList.contains("burger")||e.target.parentNode.classList.contains("burger")||document.getElementById("nav-menu-list").classList.remove("responsive-show")})}handle_chang_lang(e){a.update_translations(e).then(()=>{obj2htm.renderCycle()}).catch(e=>console.log(e))}renderHome(){return{tag:"div",class:"home",contents:[{tag:"a",href:"/",contents:[{tag:"img",alt:"Logo Kuadrado",src:`${s}/logo_kuadrado.svg`},{tag:"img",alt:"Kuadrado Software",class:"logo-text",src:`${s}/logo_kuadrado_txt.svg`}]}]}}renderMenu(e,i=!1,o=""){return{tag:"ul",id:"nav-menu-list",class:i?"submenu":"",contents:e.map(e=>{var{url:t,text:n,submenu:e}=e;const s=`${o}${t}`;return{tag:"li",class:i||window.location.pathname!==s?"":"active",contents:[{tag:"a",href:s,contents:r(n)}].concat(e?[this.renderMenu(e,!0,t)]:[])}}).concat({tag:"li",class:"lang-flags",contents:["fr","en"].map(e=>({tag:"img",src:`${s}/flag-${e}.svg`,class:a.locale===e?"selected":"",onclick:this.handle_chang_lang.bind(this,e)}))})}}renderResponsiveBurger(){return{tag:"div",class:"burger",onclick:this.handleBurgerClick.bind(this),contents:[{tag:"span",contents:"···"}]}}render(){return{tag:"nav",contents:[this.renderHome(),this.renderResponsiveBurger(),this.renderMenu(i)]}}}},{"../../../constants":3,"ks-cheap-translator":4}],47:[function(e,t,n){"use strict";const{in_construction:s}=e("../../config"),{images_url:i}=e("../../constants"),o=e("./components/navbar"),a=e("ks-cheap-translator"),r=a.trad.bind(a);t.exports=class{constructor(e){this.props=e}render(){return{tag:"main",contents:[{tag:"header",contents:[(new o).render()]},s&&{tag:"section",class:"warning-banner",contents:[{tag:"strong",class:"page-contents-center",contents:r("Site en construction ...")}]},{tag:"section",id:"page-container",contents:[this.props.page.render()]},{tag:"footer",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"logo Kuadrado",src:`${i}/logo_kuadrado.svg`},{tag:"img",class:"text-logo",alt:"Kuadrado Software",src:`${i}/logo_kuadrado_txt.svg`}]},{tag:"span",contents:"32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France"},{tag:"div",contents:[{tag:"strong",contents:"<blue>Contact : </blue>"},{tag:"a",href:"mailto:contact@kuadrado-software.fr",contents:"contact@kuadrado-software.fr"}]},{tag:"div",class:"social",contents:[{tag:"strong",contents:`<blue>${r("Sur les réseaux")} : </blue>`},{tag:"a",href:"https://www.linkedin.com/company/kuadrado-software",target:"_blank",contents:"in",title:"Linkedin"},{tag:"a",href:"https://mastodon.gamedev.place/@kuadrado_software",target:"_blank",contents:"m",title:"Mastodon"}]},{tag:"span",contents:`Copyleft 🄯 ${(new Date).getFullYear()} Kuadrado Software | + ${r("kuadrado-footer-copyleft")}`},{tag:"div",contents:[{tag:"span",contents:r("Ce site web est")+" "},{tag:"a",target:"_blank",style_rules:{fontWeight:"bold"},href:"https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md",contents:"OPEN SOURCE"}]}]}]}}}},{"../../config":2,"../../constants":3,"./components/navbar":46,"ks-cheap-translator":4}]},{},[44]); \ No newline at end of file diff --git a/public/main.js b/public/main.js index 1e3f2ac561a68051d057a398bb632213e26b0cca..f94c5f58efe6a208a5dac116f4a1d5d62b498591 100644 --- a/public/main.js +++ b/public/main.js @@ -1,846 +1,2 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ -module.exports = { - build: { - protected_dirs: ["assets", "style", "views", "standard"], - default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"], - }, -}; - -},{}],2:[function(require,module,exports){ -module.exports = { - images_url: `/assets/images`, - data_url: `/assets/data`, - translations_url: "/assets/translations" -}; - -},{}],3:[function(require,module,exports){ -"use strict"; -/** - * A tiny library to handle text translation. - */ - -/** - * init parameter object type - * @typedef TranslatorParam - * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes - * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" - * - * @property {Boolean} use_url_locale_fragment - DEFAULT: true - - * If true, the 1st fragment of the window.location url will be parsed as a language code - * Example: - * window.location - * > https://example.com/en/some-page... - * then the /en/ part of the url will be taken in priority and locale will be set to "en". - * window.location - * > https://example.com/some-page... - * No locale fragment will be found so locale will not be set from url fragment. - * window.location - * > https://example.com/some-page/en - * Doesn't work, locale fragment must be the first framgment - * - * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" - * The key used to saved the current locale into local storage - * - * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations - * Translations files are expected to be named with their corresponding locale code. - * Example: - * if supported_languages is set to ["en", "fr", "it"] - * and translations_url is set to "https://example.com/translations/"" - * Then the expected translations files are - * https://example.com/translations/en.json - * https://example.com/translations/fr.json - * https://example.com/translations/it.json - * The json resources must simple key value maps, value being the translated text. - */ - -module.exports = { - locale: "en", // ISO 639 lowercase language code - supported_languages: ["en"], - translations: {}, - translations_url: "", - use_url_locale_fragment: true, - local_storage_key: "translator-prefered-language", - - /** - * Initialize the lib with params - * @param {TranslatorParam} params - * @returns {Promise} - */ - init(params) { - Object.entries(params).forEach(k_v => { - const [key, value] = k_v; - if ([ - "supported_languages", - "use_url_locale_fragment", - "local_storage_key", - "translations_url" - ].includes(key)) { - this[key] = value; - } - }); - - this.translations_url = this.format_translations_url(this.translations_url); - this.supported_languages = this.format_supported_languages(this.supported_languages); - - return new Promise((resolve, reject) => { - const loc = - (() => {// Locale from url priority 1 - if (this.use_url_locale_fragment) { - const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - return fragment_is_locale ? first_url_fragment : ""; - } else { - return ""; - } - })() - || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 - || (() => { // locale from navigator priority 3 - const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); - return this.supported_languages.includes(navigator_locale) - ? navigator_locale - : this.supported_languages[0] // Default if navigator locale is not supported - })(); - - fetch(`${this.translations_url}${loc}.json`) - .then(response => response.json()) - .then(response => { - this.locale = loc; - this.translations = response; - resolve(); - }) - .catch(err => { - this.locale = "en"; - reject(err); - }); - }); - }, - - /** - * Return a lowercase string without dash. - * If given locale is en-EN, then "en" will be returned. - * @param {String} locale - * @returns A lowercase string - */ - format_locale(locale) { - return locale.split("-")[0].toLowerCase(); - }, - - /** - * Appends a slash at the end of the given string if missing, and returns the url. - * @param {String} url - * @returns {String} - */ - format_translations_url(url) { - if (url.charAt(url.length - 1) !== "/") { - url += "/" - } - return url; - }, - - /** - * Return the array of language codes formatted as lowsercase language code. - * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. - * @param {String[]} languages_codes - * @returns {String[]} - */ - format_supported_languages(languages_codes) { - return languages_codes.map(lc => this.format_locale(lc)); - }, - - /** - * Fetches a new set of translations in case the wanted language changes - * @param {String} locale A lowercase language code - * @returns {Promise} - */ - update_translations(locale) { - locale = this.format_locale(locale); - return new Promise((resolve, reject) => { - fetch(`${this.translations_url}${locale}.json`) - .then(response => response.json()) - .then(response => { - this.translations = response; - this.locale = locale; - - localStorage.setItem(this.local_storage_key, locale); - - const split_path = window.location.pathname.substring(1).split("/"); - const first_url_fragment = split_path[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - - if (fragment_is_locale) { - split_path.splice(0, 1, locale); - const updated_path = split_path.join("/"); - window.history.replaceState(null, "", "/" + updated_path); - } - resolve(); - }) - .catch(err => { - reject(err); - }); - }); - }, - - /** - * Tries to get the translation of the source string, or return the string as it. - * @param {String} text The source text to translate - * @param {Object} params Some dynamic element to insert in the text - * Params can be used if the translated text provide placeholder like {%some_word%} - * Example: - * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) - * -> translation for "Some trad key": "A translated text with {%some_word%}" - * -> will be return as : "A translated text with a dynamic parameter" - * @returns {String} - */ - trad: function (text, params = {}) { - text = this.translations[text] || text; - - Object.keys(params).forEach(k => { - text = text.replace(`{%${k}%}`, params[k]); - }); - - return text; - } -}; -},{}],4:[function(require,module,exports){ -"use strict"; - -module.exports = { - register_key: "objectToHtmlRender", - - /** - * Register "this" as a window scope accessible variable named by the given key, or default. - * @param {String} key - */ - register(key) { - const register_key = key || this.register_key; - window[register_key] = this; - }, - - /** - * This must be called before any other method in order to initialize the lib. - * It provides the root of the rendering cycle as a Javascript object. - * @param {Object} renderCycleRoot A JS component with a render method. - */ - setRenderCycleRoot(renderCycleRoot) { - this.renderCycleRoot = renderCycleRoot; - }, - - event_name: "objtohtml-render-cycle", - - /** - * Set a custom event name for the event that is trigger on render cycle. - * Default is "objtohtml-render-cycle". - * @param {String} evt_name - */ - setEventName(evt_name) { - this.event_name = evt_name; - }, - - /** - * This is the core agorithm that read an javascript Object and convert it into an HTML element. - * @param {Object} obj The object representing the html element must be formatted like: - * { - * tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li... - * xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag. - * See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information - * style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax, - * like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc. - * contents: Array or String // This reprensents the contents that will be nested in the created html element. - * <div>{contents}</div> - * The contents can be an array of other objects reprenting elements (with tag, contents, etc) - * or it can be a simple string. - * // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title... - * // or they can also define custom html5 attributes, like data, my_custom_attr or anything. - * } - * @returns {HTMLElement} The output html node. - */ - objectToHtml(obj) { - if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process. - const objectToHtml = this.objectToHtml.bind(this); - const { tag, xmlns } = obj; - const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag); - const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"]; - - Object.keys(obj) - .filter(attr => !excludeKeys.includes(attr)) - .forEach(attr => { - switch (attr) { - case "class": - node.classList.add(...obj[attr].split(" ").filter(s => s !== "")); - break; - case "on_render": - if (!obj.id) { - node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`; - } - if (typeof obj.on_render !== "function") { - console.error("The on_render attribute must be a function") - } else { - this.attach_on_render_callback(node, obj.on_render); - } - break; - default: - if (xmlns !== undefined) { - node.setAttributeNS(null, attr, obj[attr]) - } else { - node[attr] = obj[attr]; - } - } - }); - if (obj.contents && typeof obj.contents === "string") { - node.innerHTML = obj.contents; - } else { - obj.contents && - obj.contents.length > 0 && - obj.contents.forEach(el => { - switch (typeof el) { - case "string": - node.innerHTML = el; - break; - case "object": - if (xmlns !== undefined) { - el = Object.assign(el, { xmlns }) - } - node.appendChild(objectToHtml(el)); - break; - } - }); - } - - if (obj.style_rules) { - Object.keys(obj.style_rules).forEach(rule => { - node.style[rule] = obj.style_rules[rule]; - }); - } - - return node; - }, - - on_render_callbacks: [], - - /** - * This is called if the on_render attribute of a component is set. - * @param {HTMLElement} node The created html element - * @param {Function} callback The callback defined in the js component to render - */ - attach_on_render_callback(node, callback) { - const callback_handler = { - callback: e => { - if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) { - callback(node); - const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node))); - if (handler_index === -1) { - console.warn("A callback was registered for node with id " + node.id + " but callbacck handler is undefined.") - } else { - window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback) - this.on_render_callbacks.splice(handler_index, 1); - } - } - }, - node, - }; - - const len = this.on_render_callbacks.push(callback_handler); - window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback); - }, - - /** - * If a main element exists in the html document, it will be used as rendering root. - * If not, it will be created and inserted. - */ - renderCycle: function () { - const main_elmt = document.getElementsByTagName("main")[0] || (function () { - const created_main = document.createElement("main"); - document.body.appendChild(created_main); - return created_main; - })(); - - this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" }); - }, - - /** - * This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component, - * it can start from any component of the tree. The root component must be given as the first argument, the second argument be - * be a valid html element in the dom and will be used as the insertion target. - * @param {Object} object An object providing a render method returning an object representation of the html to insert - * @param {HTMLElement} htmlNode The htlm element to update - * @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override", - * "insert-before" (must be defined along with an insertIndex key (integer)), - * "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove". - * In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}... - */ - subRender(object, htmlNode, options = { mode: "append" }) { - let outputNode = null; - - const get_insert = () => { - outputNode = this.objectToHtml(object); - return outputNode; - }; - - switch (options.mode) { - case "append": - htmlNode.appendChild(get_insert()); - break; - case "override": - htmlNode.innerHTML = ""; - htmlNode.appendChild(get_insert()); - break; - case "insert-before": - htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]); - break; - case "adjacent": - /** - * options.insertLocation must be one of: - * - * afterbegin - * afterend - * beforebegin - * beforeend - */ - htmlNode.insertAdjacentHTML(options.insertLocation, get_insert()); - break; - case "replace": - htmlNode.parentNode.replaceChild(get_insert(), htmlNode); - break; - case "remove": - htmlNode.remove(); - break; - } - const evt_name = this.event_name; - const event = new CustomEvent(evt_name, { - detail: { - inputObject: object, - outputNode, - insertOptions: options, - targetNode: htmlNode, - } - }); - - window.dispatchEvent(event); - }, -}; -},{}],5:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../constants"); - -class ThemeCard { - constructor(props) { - this.props = props; - } - - render() { - return { - tag: "a", - class: "theme-card", - href: this.props.href, - contents: [ - { - tag: "div", - class: "card-img", - contents: [{ tag: "img", alt: `thematic image ${this.props.img.replace(/\.[A-Za-z]+/, "")}`, src: `${images_url}/${this.props.img}` }], - }, - { - tag: "div", - class: "card-title", - contents: [{ tag: "h2", class: "section-title", contents: this.props.title }], - }, - { - tag: "div", - class: "card-description", - contents: [{ tag: "p", contents: this.props.description }], - }, - ], - }; - } -} - -module.exports = ThemeCard; - -},{"../../constants":2}],6:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../constants"); -const ThemeCard = require("./home-page-components/theme-card"); -const WebPage = require("./lib/web-page"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -class HomePage extends WebPage { - constructor() { - super({ id: "home-page" }); - } - - render() { - return { - tag: "div", - id: this.id, - contents: [ - { - tag: "div", - class: "page-header", - contents: [ - { - tag: "div", - class: "big-logo page-contents-center", - contents: [ - { - tag: "img", - alt: "logo Kuadrado", - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - class: "logo-text", - alt: "Kuadrado", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - { tag: "h1", contents: "Kuadrado Software", class: "page-contents-center" }, - { - tag: "p", - class: "page-contents-center", - contents: t("kuadrado-home-description"), - }, - { - tag: "ul", - class: "philo-bubbles", - contents: [t("Simplicité"), t("Légèreté"), t("Écologie")].map(word => { - return { - tag: "li", - contents: [{ tag: "span", contents: word }], - }; - }), - }, - ], - }, - { - tag: "section", - class: "page-contents-center poles", - contents: [ - { - title: t("Jeux"), - img: "game_controller.svg", - href: "/games/", - description: - t("games-description"), - }, - { - title: t("Pédagogie"), - img: "brain.svg", - href: "/education/", - description: t("education-description"), - }, - { - title: "Software", - img: "meca_proc.svg", - href: "/software-development/", - description: t("software-description"), - }, - ].map(cardProps => new ThemeCard(cardProps).render()), - }, - ], - }; - } -} - -module.exports = HomePage; - -},{"../constants":2,"./home-page-components/theme-card":5,"./lib/web-page":7,"ks-cheap-translator":3}],7:[function(require,module,exports){ -"use strict"; -const translator = require("ks-cheap-translator"); -const { translations_url } = require("../../constants"); - -class WebPage { - constructor(args) { - Object.assign(this, args); - - if (!this.id) { - this.id = "webpage-" + performance.now(); - } - - translator.init({ - translations_url, - supported_languages: ["fr", "en"], - }).then(this.refresh_all.bind(this)); - } - - refresh() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) - } - - refresh_all() { - obj2htm.renderCycle() - } -} - -module.exports = WebPage; -},{"../../constants":2,"ks-cheap-translator":3}],8:[function(require,module,exports){ -"use strict"; - -const HomePage = require("./homepage"); -const runPage = require("./run-page"); - -runPage(HomePage); - -},{"./homepage":6,"./run-page":9}],9:[function(require,module,exports){ -"use strict"; - -const renderer = require("object-to-html-renderer") -const Template = require("./template/template"); - -module.exports = function runPage(PageComponent) { - const template = new Template({ page: new PageComponent() }); - renderer.register("obj2htm") - obj2htm.setRenderCycleRoot(template); - obj2htm.renderCycle(); -}; - -},{"./template/template":11,"object-to-html-renderer":4}],10:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -const NAV_MENU_ITEMS = [ - { url: "/games/", text: "Jeux" }, - { - url: "/education/", - text: "Pédagogie", - }, - { url: "/software-development/", text: "Software" } -]; - -class NavBar { - constructor() { - this.initEventHandlers(); - } - - handleBurgerClick() { - document.getElementById("nav-menu-list").classList.toggle("responsive-show"); - } - - initEventHandlers() { - window.addEventListener("click", event => { - if ( - event.target.id !== "nav-menu-list" && - !event.target.classList.contains("burger") && - !event.target.parentNode.classList.contains("burger") - ) { - document.getElementById("nav-menu-list").classList.remove("responsive-show"); - } - }); - } - - handle_chang_lang(lang) { - translator.update_translations(lang).then(() => { - obj2htm.renderCycle(); - }).catch(err => console.log(err)); - } - - renderHome() { - return { - tag: "div", - class: "home", - contents: [ - { - tag: "a", - href: "/", - contents: [ - { - tag: "img", - alt: "Logo Kuadrado", - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - alt: "Kuadrado Software", - class: "logo-text", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - ], - }; - } - - renderMenu(menuItemsArray, isSubmenu = false, parentUrl = "") { - return { - tag: "ul", - id: "nav-menu-list", - class: isSubmenu ? "submenu" : "", - contents: menuItemsArray.map(item => { - const { url, text, submenu } = item; - const href = `${parentUrl}${url}`; - return { - tag: "li", - class: !isSubmenu && window.location.pathname === href ? "active" : "", - contents: [ - { - tag: "a", - href, - contents: t(text), - }, - ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), - }; - }).concat({ - tag: "li", - class: "lang-flags", - contents: ["fr", "en"].map(lang => { - return { - tag: "img", src: `${images_url}/flag-${lang}.svg`, - class: translator.locale === lang ? "selected" : "", - onclick: this.handle_chang_lang.bind(this, lang) - } - }) - }), - }; - } - - renderResponsiveBurger() { - return { - tag: "div", - class: "burger", - onclick: this.handleBurgerClick.bind(this), - contents: [{ tag: "span", contents: "···" }], - }; - } - - render() { - return { - tag: "nav", - contents: [ - this.renderHome(), - this.renderResponsiveBurger(), - this.renderMenu(NAV_MENU_ITEMS), - ], - }; - } -} - -module.exports = NavBar; - -},{"../../../constants":2,"ks-cheap-translator":3}],11:[function(require,module,exports){ -"use strict"; - -const { in_construction } = require("../../config"); -const { images_url } = require("../../constants"); -const NavBar = require("./components/navbar"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator) - -class Template { - constructor(props) { - this.props = props; - } - render() { - return { - tag: "main", - contents: [ - { - tag: "header", - contents: [new NavBar().render()], - }, - in_construction && { - tag: "section", - class: "warning-banner", - contents: [ - { - tag: "strong", - class: "page-contents-center", - contents: t("Site en construction ..."), - }, - ], - }, - { - tag: "section", - id: "page-container", - contents: [this.props.page.render()], - }, - { - tag: "footer", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: `logo Kuadrado`, - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - class: "text-logo", - alt: "Kuadrado Software", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - { - tag: "span", - contents: "32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France", - }, - { - tag: "div", - contents: [ - { tag: "strong", contents: "<blue>Contact : </blue>" }, - { - tag: "a", - href: "mailto:contact@kuadrado-software.fr", - contents: "contact@kuadrado-software.fr", - }, - ], - }, - { - tag: "div", - class: "social", - contents: [ - { - tag: "strong", - contents: `<blue>${t("Sur les réseaux")} : </blue>`, - }, - { - tag: "a", - href: "https://www.linkedin.com/company/kuadrado-software", - target: "_blank", - contents: "in", - title: "Linkedin", - }, - { - tag: "a", - href: "https://mastodon.gamedev.place/@kuadrado_software", - target: "_blank", - contents: "m", - title: "Mastodon", - } - ], - }, - { - tag: "span", - contents: `Copyleft 🄯 ${new Date() - .getFullYear()} Kuadrado Software | - ${t("kuadrado-footer-copyleft")}`, - }, - { - tag: "div", contents: [ - { tag: "span", contents: t("Ce site web est") + " " }, - { - tag: "a", target: "_blank", - style_rules: { fontWeight: "bold" }, - href: "https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md", - contents: "OPEN SOURCE" - } - ] - } - ], - }, - ], - }; - } -} - -module.exports = Template; - -},{"../../config":1,"../../constants":2,"./components/navbar":10,"ks-cheap-translator":3}]},{},[8]); +!function s(a,r,o){function c(e,t){if(!r[e]){if(!a[e]){var n="function"==typeof require&&require;if(!t&&n)return n(e,!0);if(i)return i(e,!0);throw(n=new Error("Cannot find module '"+e+"'")).code="MODULE_NOT_FOUND",n}n=r[e]={exports:{}},a[e][0].call(n.exports,function(t){return c(a[e][1][t]||t)},n,n.exports,s,a,r,o)}return r[e].exports}for(var i="function"==typeof require&&require,t=0;t<o.length;t++)c(o[t]);return c}({1:[function(t,e,n){e.exports={build:{protected_dirs:["assets","style","views","standard"],default_meta_keys:["title","description","image","open_graph","json_ld"]}}},{}],2:[function(t,e,n){e.exports={images_url:"/assets/images",data_url:"/assets/data",translations_url:"/assets/translations"}},{}],3:[function(t,e,n){"use strict";e.exports={locale:"en",supported_languages:["en"],translations:{},translations_url:"",use_url_locale_fragment:!0,local_storage_key:"translator-prefered-language",init(t){return Object.entries(t).forEach(t=>{var[e,t]=t;["supported_languages","use_url_locale_fragment","local_storage_key","translations_url"].includes(e)&&(this[e]=t)}),this.translations_url=this.format_translations_url(this.translations_url),this.supported_languages=this.format_supported_languages(this.supported_languages),new Promise((e,n)=>{const s=(()=>{if(this.use_url_locale_fragment){var t=window.location.pathname.substring(1).split("/")[0];return this.supported_languages.includes(t)?t:""}return""})()||localStorage.getItem(this.local_storage_key)||(t=navigator.language.split("-")[0].toLocaleLowerCase(),this.supported_languages.includes(t)?t:this.supported_languages[0]);var t;fetch(`${this.translations_url}${s}.json`).then(t=>t.json()).then(t=>{this.locale=s,this.translations=t,e()}).catch(t=>{this.locale="en",n(t)})})},format_locale(t){return t.split("-")[0].toLowerCase()},format_translations_url(t){return"/"!==t.charAt(t.length-1)&&(t+="/"),t},format_supported_languages(t){return t.map(t=>this.format_locale(t))},update_translations(s){return s=this.format_locale(s),new Promise((n,e)=>{fetch(`${this.translations_url}${s}.json`).then(t=>t.json()).then(t=>{this.translations=t,this.locale=s,localStorage.setItem(this.local_storage_key,s);const e=window.location.pathname.substring(1).split("/");t=e[0];this.supported_languages.includes(t)&&(e.splice(0,1,s),t=e.join("/"),window.history.replaceState(null,"","/"+t)),n()}).catch(t=>{e(t)})})},trad:function(e,n={}){return e=this.translations[e]||e,Object.keys(n).forEach(t=>{e=e.replace(`{%${t}%}`,n[t])}),e}}},{}],4:[function(t,e,n){"use strict";e.exports={register_key:"objectToHtmlRender",register(t){t=t||this.register_key;window[t]=this},setRenderCycleRoot(t){this.renderCycleRoot=t},event_name:"objtohtml-render-cycle",setEventName(t){this.event_name=t},objectToHtml(e){if(!e)return document.createElement("span");const n=this.objectToHtml.bind(this),{tag:t,xmlns:s}=e,a=void 0!==s?document.createElementNS(s,t):document.createElement(t),r=["tag","contents","style_rules","state","xmlns"];return Object.keys(e).filter(t=>!r.includes(t)).forEach(t=>{switch(t){case"class":a.classList.add(...e[t].split(" ").filter(t=>""!==t));break;case"on_render":e.id||(a.id=`${btoa(JSON.stringify(e).slice(0,127)).replace(/\=/g,"")}${window.performance.now()}`),"function"!=typeof e.on_render?console.error("The on_render attribute must be a function"):this.attach_on_render_callback(a,e.on_render);break;default:void 0!==s?a.setAttributeNS(null,t,e[t]):a[t]=e[t]}}),e.contents&&"string"==typeof e.contents?a.innerHTML=e.contents:e.contents&&0<e.contents.length&&e.contents.forEach(t=>{switch(typeof t){case"string":a.innerHTML=t;break;case"object":void 0!==s&&(t=Object.assign(t,{xmlns:s})),a.appendChild(n(t))}}),e.style_rules&&Object.keys(e.style_rules).forEach(t=>{a.style[t]=e.style_rules[t]}),a},on_render_callbacks:[],attach_on_render_callback(e,n){var t={callback:t=>{t.detail.outputNode!==e&&!t.detail.outputNode.querySelector(`#${e.id}`)||(n(e),-1===(t=this.on_render_callbacks.indexOf(this.on_render_callbacks.find(t=>t.node===e)))?console.warn("A callback was registered for node with id "+e.id+" but callbacck handler is undefined."):(window.removeEventListener(this.event_name,this.on_render_callbacks[t].callback),this.on_render_callbacks.splice(t,1)))},node:e},t=this.on_render_callbacks.push(t);window.addEventListener(this.event_name,this.on_render_callbacks[t-1].callback)},renderCycle:function(){var t,t=document.getElementsByTagName("main")[0]||(t=document.createElement("main"),document.body.appendChild(t),t);this.subRender(this.renderCycleRoot.render(),t,{mode:"replace"})},subRender(t,e,n={mode:"append"}){let s=null;var a=()=>(s=this.objectToHtml(t),s);switch(n.mode){case"append":e.appendChild(a());break;case"override":e.innerHTML="",e.appendChild(a());break;case"insert-before":e.insertBefore(a(),e.childNodes[n.insertIndex]);break;case"adjacent":e.insertAdjacentHTML(n.insertLocation,a());break;case"replace":e.parentNode.replaceChild(a(),e);break;case"remove":e.remove()}var r=this.event_name,r=new CustomEvent(r,{detail:{inputObject:t,outputNode:s,insertOptions:n,targetNode:e}});window.dispatchEvent(r)}}},{}],5:[function(t,e,n){"use strict";const{images_url:s}=t("../../constants");e.exports=class{constructor(t){this.props=t}render(){return{tag:"a",class:"theme-card",href:this.props.href,contents:[{tag:"div",class:"card-img",contents:[{tag:"img",alt:`thematic image ${this.props.img.replace(/\.[A-Za-z]+/,"")}`,src:`${s}/${this.props.img}`}]},{tag:"div",class:"card-title",contents:[{tag:"h2",class:"section-title",contents:this.props.title}]},{tag:"div",class:"card-description",contents:[{tag:"p",contents:this.props.description}]}]}}}},{"../../constants":2}],6:[function(t,e,n){"use strict";const{images_url:s}=t("../constants"),a=t("./home-page-components/theme-card");var r=t("./lib/web-page");const o=t("ks-cheap-translator"),c=o.trad.bind(o);class i extends r{constructor(){super({id:"home-page"})}render(){return{tag:"div",id:this.id,contents:[{tag:"div",class:"page-header",contents:[{tag:"div",class:"big-logo page-contents-center",contents:[{tag:"img",alt:"logo Kuadrado",src:`${s}/logo_kuadrado.svg`},{tag:"img",class:"logo-text",alt:"Kuadrado",src:`${s}/logo_kuadrado_txt.svg`}]},{tag:"h1",contents:"Kuadrado Software",class:"page-contents-center"},{tag:"p",class:"page-contents-center",contents:c("kuadrado-home-description")},{tag:"ul",class:"philo-bubbles",contents:[c("Simplicité"),c("Légèreté"),c("Écologie")].map(t=>({tag:"li",contents:[{tag:"span",contents:t}]}))}]},{tag:"section",class:"page-contents-center poles",contents:[{title:c("Jeux"),img:"game_controller.svg",href:"/games/",description:c("games-description")},{title:c("Pédagogie"),img:"brain.svg",href:"/education/",description:c("education-description")},{title:"Software",img:"meca_proc.svg",href:"/software-development/",description:c("software-description")}].map(t=>new a(t).render())}]}}}e.exports=i},{"../constants":2,"./home-page-components/theme-card":5,"./lib/web-page":7,"ks-cheap-translator":3}],7:[function(t,e,n){"use strict";const s=t("ks-cheap-translator"),{translations_url:a}=t("../../constants");e.exports=class{constructor(t){Object.assign(this,t),this.id||(this.id="webpage-"+performance.now()),s.init({translations_url:a,supported_languages:["fr","en"]}).then(this.refresh_all.bind(this))}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}refresh_all(){obj2htm.renderCycle()}}},{"../../constants":2,"ks-cheap-translator":3}],8:[function(t,e,n){"use strict";var s=t("./homepage");const a=t("./run-page");a(s)},{"./homepage":6,"./run-page":9}],9:[function(t,e,n){"use strict";const s=t("object-to-html-renderer"),a=t("./template/template");e.exports=function(t){t=new a({page:new t});s.register("obj2htm"),obj2htm.setRenderCycleRoot(t),obj2htm.renderCycle()}},{"./template/template":11,"object-to-html-renderer":4}],10:[function(t,e,n){"use strict";const{images_url:s}=t("../../../constants"),o=t("ks-cheap-translator"),c=o.trad.bind(o),a=[{url:"/games/",text:"Jeux"},{url:"/education/",text:"Pédagogie"},{url:"/software-development/",text:"Software"}];e.exports=class{constructor(){this.initEventHandlers()}handleBurgerClick(){document.getElementById("nav-menu-list").classList.toggle("responsive-show")}initEventHandlers(){window.addEventListener("click",t=>{"nav-menu-list"===t.target.id||t.target.classList.contains("burger")||t.target.parentNode.classList.contains("burger")||document.getElementById("nav-menu-list").classList.remove("responsive-show")})}handle_chang_lang(t){o.update_translations(t).then(()=>{obj2htm.renderCycle()}).catch(t=>console.log(t))}renderHome(){return{tag:"div",class:"home",contents:[{tag:"a",href:"/",contents:[{tag:"img",alt:"Logo Kuadrado",src:`${s}/logo_kuadrado.svg`},{tag:"img",alt:"Kuadrado Software",class:"logo-text",src:`${s}/logo_kuadrado_txt.svg`}]}]}}renderMenu(t,a=!1,r=""){return{tag:"ul",id:"nav-menu-list",class:a?"submenu":"",contents:t.map(t=>{var{url:e,text:n,submenu:t}=t;const s=`${r}${e}`;return{tag:"li",class:a||window.location.pathname!==s?"":"active",contents:[{tag:"a",href:s,contents:c(n)}].concat(t?[this.renderMenu(t,!0,e)]:[])}}).concat({tag:"li",class:"lang-flags",contents:["fr","en"].map(t=>({tag:"img",src:`${s}/flag-${t}.svg`,class:o.locale===t?"selected":"",onclick:this.handle_chang_lang.bind(this,t)}))})}}renderResponsiveBurger(){return{tag:"div",class:"burger",onclick:this.handleBurgerClick.bind(this),contents:[{tag:"span",contents:"···"}]}}render(){return{tag:"nav",contents:[this.renderHome(),this.renderResponsiveBurger(),this.renderMenu(a)]}}}},{"../../../constants":2,"ks-cheap-translator":3}],11:[function(t,e,n){"use strict";const{in_construction:s}=t("../../config"),{images_url:a}=t("../../constants"),r=t("./components/navbar"),o=t("ks-cheap-translator"),c=o.trad.bind(o);e.exports=class{constructor(t){this.props=t}render(){return{tag:"main",contents:[{tag:"header",contents:[(new r).render()]},s&&{tag:"section",class:"warning-banner",contents:[{tag:"strong",class:"page-contents-center",contents:c("Site en construction ...")}]},{tag:"section",id:"page-container",contents:[this.props.page.render()]},{tag:"footer",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"logo Kuadrado",src:`${a}/logo_kuadrado.svg`},{tag:"img",class:"text-logo",alt:"Kuadrado Software",src:`${a}/logo_kuadrado_txt.svg`}]},{tag:"span",contents:"32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France"},{tag:"div",contents:[{tag:"strong",contents:"<blue>Contact : </blue>"},{tag:"a",href:"mailto:contact@kuadrado-software.fr",contents:"contact@kuadrado-software.fr"}]},{tag:"div",class:"social",contents:[{tag:"strong",contents:`<blue>${c("Sur les réseaux")} : </blue>`},{tag:"a",href:"https://www.linkedin.com/company/kuadrado-software",target:"_blank",contents:"in",title:"Linkedin"},{tag:"a",href:"https://mastodon.gamedev.place/@kuadrado_software",target:"_blank",contents:"m",title:"Mastodon"}]},{tag:"span",contents:`Copyleft 🄯 ${(new Date).getFullYear()} Kuadrado Software | + ${c("kuadrado-footer-copyleft")}`},{tag:"div",contents:[{tag:"span",contents:c("Ce site web est")+" "},{tag:"a",target:"_blank",style_rules:{fontWeight:"bold"},href:"https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md",contents:"OPEN SOURCE"}]}]}]}}}},{"../../config":1,"../../constants":2,"./components/navbar":10,"ks-cheap-translator":3}]},{},[8]); \ No newline at end of file diff --git a/public/software-development/software-development.js b/public/software-development/software-development.js index c2ed85689dc4575b4264884d7be01b338cdbbeba..5fa85b30fb499ad1b00531fdda24e5f56809bef2 100644 --- a/public/software-development/software-development.js +++ b/public/software-development/software-development.js @@ -1,984 +1,2 @@ -(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ -module.exports = { - images_url: "/assets/images" -} -},{}],2:[function(require,module,exports){ -module.exports = { - build: { - protected_dirs: ["assets", "style", "views", "standard"], - default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"], - }, -}; - -},{}],3:[function(require,module,exports){ -module.exports = { - images_url: `/assets/images`, - data_url: `/assets/data`, - translations_url: "/assets/translations" -}; - -},{}],4:[function(require,module,exports){ -"use strict"; -/** - * A tiny library to handle text translation. - */ - -/** - * init parameter object type - * @typedef TranslatorParam - * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes - * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" - * - * @property {Boolean} use_url_locale_fragment - DEFAULT: true - - * If true, the 1st fragment of the window.location url will be parsed as a language code - * Example: - * window.location - * > https://example.com/en/some-page... - * then the /en/ part of the url will be taken in priority and locale will be set to "en". - * window.location - * > https://example.com/some-page... - * No locale fragment will be found so locale will not be set from url fragment. - * window.location - * > https://example.com/some-page/en - * Doesn't work, locale fragment must be the first framgment - * - * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" - * The key used to saved the current locale into local storage - * - * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations - * Translations files are expected to be named with their corresponding locale code. - * Example: - * if supported_languages is set to ["en", "fr", "it"] - * and translations_url is set to "https://example.com/translations/"" - * Then the expected translations files are - * https://example.com/translations/en.json - * https://example.com/translations/fr.json - * https://example.com/translations/it.json - * The json resources must simple key value maps, value being the translated text. - */ - -module.exports = { - locale: "en", // ISO 639 lowercase language code - supported_languages: ["en"], - translations: {}, - translations_url: "", - use_url_locale_fragment: true, - local_storage_key: "translator-prefered-language", - - /** - * Initialize the lib with params - * @param {TranslatorParam} params - * @returns {Promise} - */ - init(params) { - Object.entries(params).forEach(k_v => { - const [key, value] = k_v; - if ([ - "supported_languages", - "use_url_locale_fragment", - "local_storage_key", - "translations_url" - ].includes(key)) { - this[key] = value; - } - }); - - this.translations_url = this.format_translations_url(this.translations_url); - this.supported_languages = this.format_supported_languages(this.supported_languages); - - return new Promise((resolve, reject) => { - const loc = - (() => {// Locale from url priority 1 - if (this.use_url_locale_fragment) { - const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - return fragment_is_locale ? first_url_fragment : ""; - } else { - return ""; - } - })() - || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 - || (() => { // locale from navigator priority 3 - const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); - return this.supported_languages.includes(navigator_locale) - ? navigator_locale - : this.supported_languages[0] // Default if navigator locale is not supported - })(); - - fetch(`${this.translations_url}${loc}.json`) - .then(response => response.json()) - .then(response => { - this.locale = loc; - this.translations = response; - resolve(); - }) - .catch(err => { - this.locale = "en"; - reject(err); - }); - }); - }, - - /** - * Return a lowercase string without dash. - * If given locale is en-EN, then "en" will be returned. - * @param {String} locale - * @returns A lowercase string - */ - format_locale(locale) { - return locale.split("-")[0].toLowerCase(); - }, - - /** - * Appends a slash at the end of the given string if missing, and returns the url. - * @param {String} url - * @returns {String} - */ - format_translations_url(url) { - if (url.charAt(url.length - 1) !== "/") { - url += "/" - } - return url; - }, - - /** - * Return the array of language codes formatted as lowsercase language code. - * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. - * @param {String[]} languages_codes - * @returns {String[]} - */ - format_supported_languages(languages_codes) { - return languages_codes.map(lc => this.format_locale(lc)); - }, - - /** - * Fetches a new set of translations in case the wanted language changes - * @param {String} locale A lowercase language code - * @returns {Promise} - */ - update_translations(locale) { - locale = this.format_locale(locale); - return new Promise((resolve, reject) => { - fetch(`${this.translations_url}${locale}.json`) - .then(response => response.json()) - .then(response => { - this.translations = response; - this.locale = locale; - - localStorage.setItem(this.local_storage_key, locale); - - const split_path = window.location.pathname.substring(1).split("/"); - const first_url_fragment = split_path[0]; - const fragment_is_locale = this.supported_languages.includes(first_url_fragment); - - if (fragment_is_locale) { - split_path.splice(0, 1, locale); - const updated_path = split_path.join("/"); - window.history.replaceState(null, "", "/" + updated_path); - } - resolve(); - }) - .catch(err => { - reject(err); - }); - }); - }, - - /** - * Tries to get the translation of the source string, or return the string as it. - * @param {String} text The source text to translate - * @param {Object} params Some dynamic element to insert in the text - * Params can be used if the translated text provide placeholder like {%some_word%} - * Example: - * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) - * -> translation for "Some trad key": "A translated text with {%some_word%}" - * -> will be return as : "A translated text with a dynamic parameter" - * @returns {String} - */ - trad: function (text, params = {}) { - text = this.translations[text] || text; - - Object.keys(params).forEach(k => { - text = text.replace(`{%${k}%}`, params[k]); - }); - - return text; - } -}; -},{}],5:[function(require,module,exports){ -"use strict"; - -module.exports = { - register_key: "objectToHtmlRender", - - /** - * Register "this" as a window scope accessible variable named by the given key, or default. - * @param {String} key - */ - register(key) { - const register_key = key || this.register_key; - window[register_key] = this; - }, - - /** - * This must be called before any other method in order to initialize the lib. - * It provides the root of the rendering cycle as a Javascript object. - * @param {Object} renderCycleRoot A JS component with a render method. - */ - setRenderCycleRoot(renderCycleRoot) { - this.renderCycleRoot = renderCycleRoot; - }, - - event_name: "objtohtml-render-cycle", - - /** - * Set a custom event name for the event that is trigger on render cycle. - * Default is "objtohtml-render-cycle". - * @param {String} evt_name - */ - setEventName(evt_name) { - this.event_name = evt_name; - }, - - /** - * This is the core agorithm that read an javascript Object and convert it into an HTML element. - * @param {Object} obj The object representing the html element must be formatted like: - * { - * tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li... - * xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag. - * See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information - * style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax, - * like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc. - * contents: Array or String // This reprensents the contents that will be nested in the created html element. - * <div>{contents}</div> - * The contents can be an array of other objects reprenting elements (with tag, contents, etc) - * or it can be a simple string. - * // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title... - * // or they can also define custom html5 attributes, like data, my_custom_attr or anything. - * } - * @returns {HTMLElement} The output html node. - */ - objectToHtml(obj) { - if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process. - const objectToHtml = this.objectToHtml.bind(this); - const { tag, xmlns } = obj; - const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag); - const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"]; - - Object.keys(obj) - .filter(attr => !excludeKeys.includes(attr)) - .forEach(attr => { - switch (attr) { - case "class": - node.classList.add(...obj[attr].split(" ").filter(s => s !== "")); - break; - case "on_render": - if (!obj.id) { - node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`; - } - if (typeof obj.on_render !== "function") { - console.error("The on_render attribute must be a function") - } else { - this.attach_on_render_callback(node, obj.on_render); - } - break; - default: - if (xmlns !== undefined) { - node.setAttributeNS(null, attr, obj[attr]) - } else { - node[attr] = obj[attr]; - } - } - }); - if (obj.contents && typeof obj.contents === "string") { - node.innerHTML = obj.contents; - } else { - obj.contents && - obj.contents.length > 0 && - obj.contents.forEach(el => { - switch (typeof el) { - case "string": - node.innerHTML = el; - break; - case "object": - if (xmlns !== undefined) { - el = Object.assign(el, { xmlns }) - } - node.appendChild(objectToHtml(el)); - break; - } - }); - } - - if (obj.style_rules) { - Object.keys(obj.style_rules).forEach(rule => { - node.style[rule] = obj.style_rules[rule]; - }); - } - - return node; - }, - - on_render_callbacks: [], - - /** - * This is called if the on_render attribute of a component is set. - * @param {HTMLElement} node The created html element - * @param {Function} callback The callback defined in the js component to render - */ - attach_on_render_callback(node, callback) { - const callback_handler = { - callback: e => { - if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) { - callback(node); - const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node))); - if (handler_index === -1) { - console.warn("A callback was registered for node with id " + node.id + " but callbacck handler is undefined.") - } else { - window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback) - this.on_render_callbacks.splice(handler_index, 1); - } - } - }, - node, - }; - - const len = this.on_render_callbacks.push(callback_handler); - window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback); - }, - - /** - * If a main element exists in the html document, it will be used as rendering root. - * If not, it will be created and inserted. - */ - renderCycle: function () { - const main_elmt = document.getElementsByTagName("main")[0] || (function () { - const created_main = document.createElement("main"); - document.body.appendChild(created_main); - return created_main; - })(); - - this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" }); - }, - - /** - * This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component, - * it can start from any component of the tree. The root component must be given as the first argument, the second argument be - * be a valid html element in the dom and will be used as the insertion target. - * @param {Object} object An object providing a render method returning an object representation of the html to insert - * @param {HTMLElement} htmlNode The htlm element to update - * @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override", - * "insert-before" (must be defined along with an insertIndex key (integer)), - * "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove". - * In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}... - */ - subRender(object, htmlNode, options = { mode: "append" }) { - let outputNode = null; - - const get_insert = () => { - outputNode = this.objectToHtml(object); - return outputNode; - }; - - switch (options.mode) { - case "append": - htmlNode.appendChild(get_insert()); - break; - case "override": - htmlNode.innerHTML = ""; - htmlNode.appendChild(get_insert()); - break; - case "insert-before": - htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]); - break; - case "adjacent": - /** - * options.insertLocation must be one of: - * - * afterbegin - * afterend - * beforebegin - * beforeend - */ - htmlNode.insertAdjacentHTML(options.insertLocation, get_insert()); - break; - case "replace": - htmlNode.parentNode.replaceChild(get_insert(), htmlNode); - break; - case "remove": - htmlNode.remove(); - break; - } - const evt_name = this.event_name; - const event = new CustomEvent(evt_name, { - detail: { - inputObject: object, - outputNode, - insertOptions: options, - targetNode: htmlNode, - } - }); - - window.dispatchEvent(event); - }, -}; -},{}],6:[function(require,module,exports){ -"use strict"; - -const { fetch_json_or_error_text } = require("./fetch"); - -function getArticleBody(text) { - return text.replaceAll("\n", "<br/>"); -} - -function getArticleDate(date) { - return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; -} - -function loadArticles(category, locale) { - return fetch_json_or_error_text(`/articles/${category}/${locale}`); -} - -module.exports = { - loadArticles, - getArticleBody, - getArticleDate, -}; - -},{"./fetch":7}],7:[function(require,module,exports){ -"use strict"; - -function fetchjson(url) { - return new Promise((resolve, reject) => { - fetch(url) - .then(r => r.json()) - .then(r => resolve(r)) - .catch(e => reject(e)); - }); -} - -function fetchtext(url) { - return new Promise((resolve, reject) => { - fetch(url) - .then(r => r.text()) - .then(r => resolve(r)) - .catch(e => reject(e)); - }); -} - -async function fetch_json_or_error_text(url, options = {}) { - return new Promise((resolve, reject) => { - fetch(url, options).then(async res => { - if (res.status >= 400 && res.status < 600) { - reject(await res.text()); - } else { - resolve(await res.json()); - } - }) - }) -} - -module.exports = { - fetchjson, - fetchtext, - fetch_json_or_error_text, -}; - -},{}],8:[function(require,module,exports){ -"use strict"; -const translator = require("ks-cheap-translator"); -const { translations_url } = require("../../constants"); - -class WebPage { - constructor(args) { - Object.assign(this, args); - - if (!this.id) { - this.id = "webpage-" + performance.now(); - } - - translator.init({ - translations_url, - supported_languages: ["fr", "en"], - }).then(this.refresh_all.bind(this)); - } - - refresh() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) - } - - refresh_all() { - obj2htm.renderCycle() - } -} - -module.exports = WebPage; -},{"../../constants":3,"ks-cheap-translator":4}],9:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../../../admin-frontend/src/constants"); -const { getArticleBody } = require("../../../lib/article-utils"); - -class SoftwareArticle { - constructor(props) { - this.props = props; - } - - render() { - const { title, body, subtitle, images, details = [] } = this.props; - - return { - tag: "article", - class: "software-article", - typeof: "SoftwareApplication", - additionalType: "Article", - contents: [ - { - tag: "h2", - class: "software-title", - contents: title, - property: "name", - }, - { - tag: "div", class: "software-image", - contents: [ - { - tag: "img", src: `${images_url}/${images[0]}` - } - ] - }, - { - tag: "h3", - class: "software-subtitle", - contents: subtitle, - property: "alternativeHeadline", - }, - { - tag: "div", - class: "software-description", - contents: getArticleBody(body), - property: "description", - }, - details.length > 0 && { - tag: "div", - class: "article-details", - contents: [ - { - tag: "h2", - contents: "Details", - }, - { - tag: "ul", - class: "details-list", - contents: details.map(detail => { - return { - tag: "li", - class: "detail", - contents: [ - { tag: "label", contents: detail.label }, - { - tag: "div", - class: "detail-value", - contents: detail.value - }, - ], - }; - }), - }, - ], - }, - ], - }; - } -} - -module.exports = SoftwareArticle; -},{"../../../../../admin-frontend/src/constants":1,"../../../lib/article-utils":6}],10:[function(require,module,exports){ -"use strict"; - -const { loadArticles } = require("../../../lib/article-utils"); -const SoftwareArticle = require("./software-article"); -const translator = require("ks-cheap-translator"); - -class SoftwareArticles { - constructor(props) { - this.props = props; - this.state = { - articles: [], - }; - this.id = "software-articles-section"; - this.loadArticles(); - } - - loadArticles() { - loadArticles("software", translator.locale).then(articles => { - this.state.articles = articles; - this.refresh(); - this.fixScroll(); - }).catch(e => console.log(e)) - } - - renderPlaceholder() { - return { - tag: "article", - class: "placeholder", - contents: [ - { tag: "div", class: "title" }, - { tag: "div", class: "body" }, - { tag: "div", class: "details" }, - ], - }; - } - - refresh() { - obj2htm.subRender(this.render(), document.getElementById(this.id), { - mode: "replace", - }); - } - - fixScroll() { - if (window.location.href.includes("#")) { - window.scrollTo( - 0, - document.getElementById(window.location.href.match(/#.+/)[0].replace("#", "")) - .offsetTop - ); - } - } - - render() { - const { articles } = this.state; - return { - tag: "section", - class: "software-articles page-contents-center", - id: this.id, - contents: - articles.length > 0 - ? articles.map(article => new SoftwareArticle({ ...article }).render()) - : [this.renderPlaceholder()], - }; - } -} - -module.exports = SoftwareArticles; - -},{"../../../lib/article-utils":6,"./software-article":9,"ks-cheap-translator":4}],11:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const WebPage = require("../../lib/web-page"); -const SoftwareArticles = require("./components/software-articles"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -class SoftwareDevelopment extends WebPage { - render() { - return { - tag: "div", - id: "software-page", - contents: [ - { - tag: "div", - class: "page-header logo-left", - contents: [ - { - tag: "div", - class: "page-contents-center grid-wrapper", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: `image mechanic electronic`, - src: `${images_url}/meca_proc.svg`, - }, - ], - }, - { tag: "h1", contents: "Software" }, - { - tag: "p", - contents: t("software-page-intro"), - }, - ], - }, - ], - }, - new SoftwareArticles().render(), - ], - }; - } -} - -module.exports = SoftwareDevelopment; - -},{"../../../constants":3,"../../lib/web-page":8,"./components/software-articles":10,"ks-cheap-translator":4}],12:[function(require,module,exports){ -"use strict"; - -"use strict"; -const runPage = require("../../run-page"); -const SoftwareDevelopment = require("./software-development"); -runPage(SoftwareDevelopment); - -},{"../../run-page":13,"./software-development":11}],13:[function(require,module,exports){ -"use strict"; - -const renderer = require("object-to-html-renderer") -const Template = require("./template/template"); - -module.exports = function runPage(PageComponent) { - const template = new Template({ page: new PageComponent() }); - renderer.register("obj2htm") - obj2htm.setRenderCycleRoot(template); - obj2htm.renderCycle(); -}; - -},{"./template/template":15,"object-to-html-renderer":5}],14:[function(require,module,exports){ -"use strict"; - -const { images_url } = require("../../../constants"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator); - -const NAV_MENU_ITEMS = [ - { url: "/games/", text: "Jeux" }, - { - url: "/education/", - text: "Pédagogie", - }, - { url: "/software-development/", text: "Software" } -]; - -class NavBar { - constructor() { - this.initEventHandlers(); - } - - handleBurgerClick() { - document.getElementById("nav-menu-list").classList.toggle("responsive-show"); - } - - initEventHandlers() { - window.addEventListener("click", event => { - if ( - event.target.id !== "nav-menu-list" && - !event.target.classList.contains("burger") && - !event.target.parentNode.classList.contains("burger") - ) { - document.getElementById("nav-menu-list").classList.remove("responsive-show"); - } - }); - } - - handle_chang_lang(lang) { - translator.update_translations(lang).then(() => { - obj2htm.renderCycle(); - }).catch(err => console.log(err)); - } - - renderHome() { - return { - tag: "div", - class: "home", - contents: [ - { - tag: "a", - href: "/", - contents: [ - { - tag: "img", - alt: "Logo Kuadrado", - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - alt: "Kuadrado Software", - class: "logo-text", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - ], - }; - } - - renderMenu(menuItemsArray, isSubmenu = false, parentUrl = "") { - return { - tag: "ul", - id: "nav-menu-list", - class: isSubmenu ? "submenu" : "", - contents: menuItemsArray.map(item => { - const { url, text, submenu } = item; - const href = `${parentUrl}${url}`; - return { - tag: "li", - class: !isSubmenu && window.location.pathname === href ? "active" : "", - contents: [ - { - tag: "a", - href, - contents: t(text), - }, - ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), - }; - }).concat({ - tag: "li", - class: "lang-flags", - contents: ["fr", "en"].map(lang => { - return { - tag: "img", src: `${images_url}/flag-${lang}.svg`, - class: translator.locale === lang ? "selected" : "", - onclick: this.handle_chang_lang.bind(this, lang) - } - }) - }), - }; - } - - renderResponsiveBurger() { - return { - tag: "div", - class: "burger", - onclick: this.handleBurgerClick.bind(this), - contents: [{ tag: "span", contents: "···" }], - }; - } - - render() { - return { - tag: "nav", - contents: [ - this.renderHome(), - this.renderResponsiveBurger(), - this.renderMenu(NAV_MENU_ITEMS), - ], - }; - } -} - -module.exports = NavBar; - -},{"../../../constants":3,"ks-cheap-translator":4}],15:[function(require,module,exports){ -"use strict"; - -const { in_construction } = require("../../config"); -const { images_url } = require("../../constants"); -const NavBar = require("./components/navbar"); -const translator = require("ks-cheap-translator"); -const t = translator.trad.bind(translator) - -class Template { - constructor(props) { - this.props = props; - } - render() { - return { - tag: "main", - contents: [ - { - tag: "header", - contents: [new NavBar().render()], - }, - in_construction && { - tag: "section", - class: "warning-banner", - contents: [ - { - tag: "strong", - class: "page-contents-center", - contents: t("Site en construction ..."), - }, - ], - }, - { - tag: "section", - id: "page-container", - contents: [this.props.page.render()], - }, - { - tag: "footer", - contents: [ - { - tag: "div", - class: "logo", - contents: [ - { - tag: "img", - alt: `logo Kuadrado`, - src: `${images_url}/logo_kuadrado.svg`, - }, - { - tag: "img", - class: "text-logo", - alt: "Kuadrado Software", - src: `${images_url}/logo_kuadrado_txt.svg`, - }, - ], - }, - { - tag: "span", - contents: "32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France", - }, - { - tag: "div", - contents: [ - { tag: "strong", contents: "<blue>Contact : </blue>" }, - { - tag: "a", - href: "mailto:contact@kuadrado-software.fr", - contents: "contact@kuadrado-software.fr", - }, - ], - }, - { - tag: "div", - class: "social", - contents: [ - { - tag: "strong", - contents: `<blue>${t("Sur les réseaux")} : </blue>`, - }, - { - tag: "a", - href: "https://www.linkedin.com/company/kuadrado-software", - target: "_blank", - contents: "in", - title: "Linkedin", - }, - { - tag: "a", - href: "https://mastodon.gamedev.place/@kuadrado_software", - target: "_blank", - contents: "m", - title: "Mastodon", - } - ], - }, - { - tag: "span", - contents: `Copyleft 🄯 ${new Date() - .getFullYear()} Kuadrado Software | - ${t("kuadrado-footer-copyleft")}`, - }, - { - tag: "div", contents: [ - { tag: "span", contents: t("Ce site web est") + " " }, - { - tag: "a", target: "_blank", - style_rules: { fontWeight: "bold" }, - href: "https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md", - contents: "OPEN SOURCE" - } - ] - } - ], - }, - ], - }; - } -} - -module.exports = Template; - -},{"../../config":2,"../../constants":3,"./components/navbar":14,"ks-cheap-translator":4}]},{},[12]); +!function s(a,r,o){function c(e,t){if(!r[e]){if(!a[e]){var n="function"==typeof require&&require;if(!t&&n)return n(e,!0);if(i)return i(e,!0);throw(n=new Error("Cannot find module '"+e+"'")).code="MODULE_NOT_FOUND",n}n=r[e]={exports:{}},a[e][0].call(n.exports,function(t){return c(a[e][1][t]||t)},n,n.exports,s,a,r,o)}return r[e].exports}for(var i="function"==typeof require&&require,t=0;t<o.length;t++)c(o[t]);return c}({1:[function(t,e,n){e.exports={images_url:"/assets/images"}},{}],2:[function(t,e,n){e.exports={build:{protected_dirs:["assets","style","views","standard"],default_meta_keys:["title","description","image","open_graph","json_ld"]}}},{}],3:[function(t,e,n){e.exports={images_url:"/assets/images",data_url:"/assets/data",translations_url:"/assets/translations"}},{}],4:[function(t,e,n){"use strict";e.exports={locale:"en",supported_languages:["en"],translations:{},translations_url:"",use_url_locale_fragment:!0,local_storage_key:"translator-prefered-language",init(t){return Object.entries(t).forEach(t=>{var[e,t]=t;["supported_languages","use_url_locale_fragment","local_storage_key","translations_url"].includes(e)&&(this[e]=t)}),this.translations_url=this.format_translations_url(this.translations_url),this.supported_languages=this.format_supported_languages(this.supported_languages),new Promise((e,n)=>{const s=(()=>{if(this.use_url_locale_fragment){var t=window.location.pathname.substring(1).split("/")[0];return this.supported_languages.includes(t)?t:""}return""})()||localStorage.getItem(this.local_storage_key)||(t=navigator.language.split("-")[0].toLocaleLowerCase(),this.supported_languages.includes(t)?t:this.supported_languages[0]);var t;fetch(`${this.translations_url}${s}.json`).then(t=>t.json()).then(t=>{this.locale=s,this.translations=t,e()}).catch(t=>{this.locale="en",n(t)})})},format_locale(t){return t.split("-")[0].toLowerCase()},format_translations_url(t){return"/"!==t.charAt(t.length-1)&&(t+="/"),t},format_supported_languages(t){return t.map(t=>this.format_locale(t))},update_translations(s){return s=this.format_locale(s),new Promise((n,e)=>{fetch(`${this.translations_url}${s}.json`).then(t=>t.json()).then(t=>{this.translations=t,this.locale=s,localStorage.setItem(this.local_storage_key,s);const e=window.location.pathname.substring(1).split("/");t=e[0];this.supported_languages.includes(t)&&(e.splice(0,1,s),t=e.join("/"),window.history.replaceState(null,"","/"+t)),n()}).catch(t=>{e(t)})})},trad:function(e,n={}){return e=this.translations[e]||e,Object.keys(n).forEach(t=>{e=e.replace(`{%${t}%}`,n[t])}),e}}},{}],5:[function(t,e,n){"use strict";e.exports={register_key:"objectToHtmlRender",register(t){t=t||this.register_key;window[t]=this},setRenderCycleRoot(t){this.renderCycleRoot=t},event_name:"objtohtml-render-cycle",setEventName(t){this.event_name=t},objectToHtml(e){if(!e)return document.createElement("span");const n=this.objectToHtml.bind(this),{tag:t,xmlns:s}=e,a=void 0!==s?document.createElementNS(s,t):document.createElement(t),r=["tag","contents","style_rules","state","xmlns"];return Object.keys(e).filter(t=>!r.includes(t)).forEach(t=>{switch(t){case"class":a.classList.add(...e[t].split(" ").filter(t=>""!==t));break;case"on_render":e.id||(a.id=`${btoa(JSON.stringify(e).slice(0,127)).replace(/\=/g,"")}${window.performance.now()}`),"function"!=typeof e.on_render?console.error("The on_render attribute must be a function"):this.attach_on_render_callback(a,e.on_render);break;default:void 0!==s?a.setAttributeNS(null,t,e[t]):a[t]=e[t]}}),e.contents&&"string"==typeof e.contents?a.innerHTML=e.contents:e.contents&&0<e.contents.length&&e.contents.forEach(t=>{switch(typeof t){case"string":a.innerHTML=t;break;case"object":void 0!==s&&(t=Object.assign(t,{xmlns:s})),a.appendChild(n(t))}}),e.style_rules&&Object.keys(e.style_rules).forEach(t=>{a.style[t]=e.style_rules[t]}),a},on_render_callbacks:[],attach_on_render_callback(e,n){var t={callback:t=>{t.detail.outputNode!==e&&!t.detail.outputNode.querySelector(`#${e.id}`)||(n(e),-1===(t=this.on_render_callbacks.indexOf(this.on_render_callbacks.find(t=>t.node===e)))?console.warn("A callback was registered for node with id "+e.id+" but callbacck handler is undefined."):(window.removeEventListener(this.event_name,this.on_render_callbacks[t].callback),this.on_render_callbacks.splice(t,1)))},node:e},t=this.on_render_callbacks.push(t);window.addEventListener(this.event_name,this.on_render_callbacks[t-1].callback)},renderCycle:function(){var t,t=document.getElementsByTagName("main")[0]||(t=document.createElement("main"),document.body.appendChild(t),t);this.subRender(this.renderCycleRoot.render(),t,{mode:"replace"})},subRender(t,e,n={mode:"append"}){let s=null;var a=()=>(s=this.objectToHtml(t),s);switch(n.mode){case"append":e.appendChild(a());break;case"override":e.innerHTML="",e.appendChild(a());break;case"insert-before":e.insertBefore(a(),e.childNodes[n.insertIndex]);break;case"adjacent":e.insertAdjacentHTML(n.insertLocation,a());break;case"replace":e.parentNode.replaceChild(a(),e);break;case"remove":e.remove()}var r=this.event_name,r=new CustomEvent(r,{detail:{inputObject:t,outputNode:s,insertOptions:n,targetNode:e}});window.dispatchEvent(r)}}},{}],6:[function(t,e,n){"use strict";const{fetch_json_or_error_text:s}=t("./fetch");e.exports={loadArticles:function(t,e){return s(`/articles/${t}/${e}`)},getArticleBody:function(t){return t.replaceAll("\n","<br/>")},getArticleDate:function(t){return`${t.getDate()}-${t.getMonth()+1}-${t.getFullYear()}`}}},{"./fetch":7}],7:[function(t,e,n){"use strict";e.exports={fetchjson:function(t){return new Promise((e,n)=>{fetch(t).then(t=>t.json()).then(t=>e(t)).catch(t=>n(t))})},fetchtext:function(t){return new Promise((e,n)=>{fetch(t).then(t=>t.text()).then(t=>e(t)).catch(t=>n(t))})},fetch_json_or_error_text:async function(t,s={}){return new Promise((e,n)=>{fetch(t,s).then(async t=>{400<=t.status&&t.status<600?n(await t.text()):e(await t.json())})})}}},{}],8:[function(t,e,n){"use strict";const s=t("ks-cheap-translator"),{translations_url:a}=t("../../constants");e.exports=class{constructor(t){Object.assign(this,t),this.id||(this.id="webpage-"+performance.now()),s.init({translations_url:a,supported_languages:["fr","en"]}).then(this.refresh_all.bind(this))}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}refresh_all(){obj2htm.renderCycle()}}},{"../../constants":3,"ks-cheap-translator":4}],9:[function(t,e,n){"use strict";const{images_url:r}=t("../../../../../admin-frontend/src/constants"),{getArticleBody:o}=t("../../../lib/article-utils");e.exports=class{constructor(t){this.props=t}render(){const{title:t,body:e,subtitle:n,images:s,details:a=[]}=this.props;return{tag:"article",class:"software-article",typeof:"SoftwareApplication",additionalType:"Article",contents:[{tag:"h2",class:"software-title",contents:t,property:"name"},{tag:"div",class:"software-image",contents:[{tag:"img",src:`${r}/${s[0]}`}]},{tag:"h3",class:"software-subtitle",contents:n,property:"alternativeHeadline"},{tag:"div",class:"software-description",contents:o(e),property:"description"},0<a.length&&{tag:"div",class:"article-details",contents:[{tag:"h2",contents:"Details"},{tag:"ul",class:"details-list",contents:a.map(t=>({tag:"li",class:"detail",contents:[{tag:"label",contents:t.label},{tag:"div",class:"detail-value",contents:t.value}]}))}]}]}}}},{"../../../../../admin-frontend/src/constants":1,"../../../lib/article-utils":6}],10:[function(t,e,n){"use strict";const{loadArticles:s}=t("../../../lib/article-utils"),a=t("./software-article"),r=t("ks-cheap-translator");e.exports=class{constructor(t){this.props=t,this.state={articles:[]},this.id="software-articles-section",this.loadArticles()}loadArticles(){s("software",r.locale).then(t=>{this.state.articles=t,this.refresh(),this.fixScroll()}).catch(t=>console.log(t))}renderPlaceholder(){return{tag:"article",class:"placeholder",contents:[{tag:"div",class:"title"},{tag:"div",class:"body"},{tag:"div",class:"details"}]}}refresh(){obj2htm.subRender(this.render(),document.getElementById(this.id),{mode:"replace"})}fixScroll(){window.location.href.includes("#")&&window.scrollTo(0,document.getElementById(window.location.href.match(/#.+/)[0].replace("#","")).offsetTop)}render(){const{articles:t}=this.state;return{tag:"section",class:"software-articles page-contents-center",id:this.id,contents:0<t.length?t.map(t=>new a({...t}).render()):[this.renderPlaceholder()]}}}},{"../../../lib/article-utils":6,"./software-article":9,"ks-cheap-translator":4}],11:[function(t,e,n){"use strict";const{images_url:s}=t("../../../constants");var a=t("../../lib/web-page");const r=t("./components/software-articles"),o=t("ks-cheap-translator"),c=o.trad.bind(o);class i extends a{render(){return{tag:"div",id:"software-page",contents:[{tag:"div",class:"page-header logo-left",contents:[{tag:"div",class:"page-contents-center grid-wrapper",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"image mechanic electronic",src:`${s}/meca_proc.svg`}]},{tag:"h1",contents:"Software"},{tag:"p",contents:c("software-page-intro")}]}]},(new r).render()]}}}e.exports=i},{"../../../constants":3,"../../lib/web-page":8,"./components/software-articles":10,"ks-cheap-translator":4}],12:[function(t,e,n){"use strict";const s=t("../../run-page");t=t("./software-development");s(t)},{"../../run-page":13,"./software-development":11}],13:[function(t,e,n){"use strict";const s=t("object-to-html-renderer"),a=t("./template/template");e.exports=function(t){t=new a({page:new t});s.register("obj2htm"),obj2htm.setRenderCycleRoot(t),obj2htm.renderCycle()}},{"./template/template":15,"object-to-html-renderer":5}],14:[function(t,e,n){"use strict";const{images_url:s}=t("../../../constants"),o=t("ks-cheap-translator"),c=o.trad.bind(o),a=[{url:"/games/",text:"Jeux"},{url:"/education/",text:"Pédagogie"},{url:"/software-development/",text:"Software"}];e.exports=class{constructor(){this.initEventHandlers()}handleBurgerClick(){document.getElementById("nav-menu-list").classList.toggle("responsive-show")}initEventHandlers(){window.addEventListener("click",t=>{"nav-menu-list"===t.target.id||t.target.classList.contains("burger")||t.target.parentNode.classList.contains("burger")||document.getElementById("nav-menu-list").classList.remove("responsive-show")})}handle_chang_lang(t){o.update_translations(t).then(()=>{obj2htm.renderCycle()}).catch(t=>console.log(t))}renderHome(){return{tag:"div",class:"home",contents:[{tag:"a",href:"/",contents:[{tag:"img",alt:"Logo Kuadrado",src:`${s}/logo_kuadrado.svg`},{tag:"img",alt:"Kuadrado Software",class:"logo-text",src:`${s}/logo_kuadrado_txt.svg`}]}]}}renderMenu(t,a=!1,r=""){return{tag:"ul",id:"nav-menu-list",class:a?"submenu":"",contents:t.map(t=>{var{url:e,text:n,submenu:t}=t;const s=`${r}${e}`;return{tag:"li",class:a||window.location.pathname!==s?"":"active",contents:[{tag:"a",href:s,contents:c(n)}].concat(t?[this.renderMenu(t,!0,e)]:[])}}).concat({tag:"li",class:"lang-flags",contents:["fr","en"].map(t=>({tag:"img",src:`${s}/flag-${t}.svg`,class:o.locale===t?"selected":"",onclick:this.handle_chang_lang.bind(this,t)}))})}}renderResponsiveBurger(){return{tag:"div",class:"burger",onclick:this.handleBurgerClick.bind(this),contents:[{tag:"span",contents:"···"}]}}render(){return{tag:"nav",contents:[this.renderHome(),this.renderResponsiveBurger(),this.renderMenu(a)]}}}},{"../../../constants":3,"ks-cheap-translator":4}],15:[function(t,e,n){"use strict";const{in_construction:s}=t("../../config"),{images_url:a}=t("../../constants"),r=t("./components/navbar"),o=t("ks-cheap-translator"),c=o.trad.bind(o);e.exports=class{constructor(t){this.props=t}render(){return{tag:"main",contents:[{tag:"header",contents:[(new r).render()]},s&&{tag:"section",class:"warning-banner",contents:[{tag:"strong",class:"page-contents-center",contents:c("Site en construction ...")}]},{tag:"section",id:"page-container",contents:[this.props.page.render()]},{tag:"footer",contents:[{tag:"div",class:"logo",contents:[{tag:"img",alt:"logo Kuadrado",src:`${a}/logo_kuadrado.svg`},{tag:"img",class:"text-logo",alt:"Kuadrado Software",src:`${a}/logo_kuadrado_txt.svg`}]},{tag:"span",contents:"32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France"},{tag:"div",contents:[{tag:"strong",contents:"<blue>Contact : </blue>"},{tag:"a",href:"mailto:contact@kuadrado-software.fr",contents:"contact@kuadrado-software.fr"}]},{tag:"div",class:"social",contents:[{tag:"strong",contents:`<blue>${c("Sur les réseaux")} : </blue>`},{tag:"a",href:"https://www.linkedin.com/company/kuadrado-software",target:"_blank",contents:"in",title:"Linkedin"},{tag:"a",href:"https://mastodon.gamedev.place/@kuadrado_software",target:"_blank",contents:"m",title:"Mastodon"}]},{tag:"span",contents:`Copyleft 🄯 ${(new Date).getFullYear()} Kuadrado Software | + ${c("kuadrado-footer-copyleft")}`},{tag:"div",contents:[{tag:"span",contents:c("Ce site web est")+" "},{tag:"a",target:"_blank",style_rules:{fontWeight:"bold"},href:"https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md",contents:"OPEN SOURCE"}]}]}]}}}},{"../../config":2,"../../constants":3,"./components/navbar":14,"ks-cheap-translator":4}]},{},[12]); \ No newline at end of file diff --git a/public/style/style.css b/public/style/style.css deleted file mode 100644 index db7b101962d4bf8d3e1fd5c1228b671f0fea983d..0000000000000000000000000000000000000000 --- a/public/style/style.css +++ /dev/null @@ -1,987 +0,0 @@ -body { - font-family: Arial, Helvetica, sans-serif; - margin: 0; -} -body * { - box-sizing: border-box; - color: #35393c; - line-height: 1.3em; -} -body ul { - margin: 0; - padding: 0; - list-style-type: none; -} -body a { - color: #4baabb; - text-decoration: none; -} -body a:hover { - color: #72e3f0; -} -body blue { - color: #4baabb; -} -body red { - color: #9c3030; -} -body green { - color: #368736; -} -body emoji { - font-style: initial; - font-size: 25px; -} -body .bg-blue { - background-color: #4baabb; - color: white; -} -body .bg-dark { - background-color: #3c4144; - color: #aabbc8; -} -body #seo-title { - visibility: hidden; -} -body img.pixelated { - image-rendering: pixelated; - image-rendering: -moz-crisp-edges; - image-rendering: crisp-edges; -} - -main { - display: flex; - flex-direction: column; - align-items: center; - min-height: 100vh; -} -main .warning-banner { - background: url("/assets/images/wallpaper_warning.svg"); - width: 100%; - height: 40px; - padding: 20px 10%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; -} -main .warning-banner strong { - font-size: 18px; - color: #1c3db2; -} -main .image-carousel { - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - background-color: black; - position: relative; -} -main .image-carousel img { - position: absolute; - object-fit: contain; - height: 80%; - max-width: 100%; -} -main .image-carousel .carousel-bullets { - position: absolute; - bottom: 0; - padding: 20px; - display: flex; - gap: 10px; -} -main .image-carousel .carousel-bullets .bullet { - cursor: pointer; - width: 8px; - height: 8px; - background-color: #6b7880; - border-radius: 100%; - box-shadow: 0 0 3px black; -} -main .image-carousel .carousel-bullets .bullet.active { - background-color: #d4d9dd; -} -@media screen and (max-width: 900px) { - main .image-carousel .carousel-bullets { - gap: 30px; - } - main .image-carousel .carousel-bullets .bullet { - width: 12px; - height: 12px; - } -} -main header { - width: 100%; - background-color: white; - position: sticky; - position: -webkit-sticky; - top: 0; - z-index: 10; -} -main header nav { - display: flex; - align-items: center; - height: 60px; -} -main header nav .home { - margin: 0 10px; -} -main header nav .home a { - display: flex; - align-items: center; - gap: 10px; -} -main header nav .home a img { - height: 40px; - width: auto; -} -main header nav .home a img.logo-text { - width: 120px; - height: auto; -} -main header nav ul { - display: flex; - padding: 0; - margin: 0; - list-style-type: none; - height: 100%; - flex: 1; -} -main header nav ul li { - position: relative; -} -main header nav ul li a { - display: flex; - align-items: center; - height: 100%; - padding: 10px 20px; - color: #96a5ae; - font-weight: 800; - text-decoration: none; -} -main header nav ul li .submenu { - visibility: hidden; - overflow: hidden; - position: absolute; - height: auto; - max-height: 0; - transition: max-height 0.6s; - top: 100%; - left: 50%; - flex-direction: column; - background-color: white; - white-space: nowrap; -} -main header nav ul li.active a { - color: #3c4144; - border-bottom: 3px solid; -} -main header nav ul li:hover a { - color: #3c4144; -} -main header nav ul li:hover .submenu { - visibility: unset; - max-height: 1000px; -} -main header nav ul li:hover .submenu a { - color: #96a5ae; - border: none; -} -main header nav ul li:hover .submenu li:hover a { - color: #3c4144; -} -main header nav ul li.lang-flags { - display: flex; - align-items: center; - gap: 10px; - margin-left: auto; - padding: 0 20px; -} -main header nav ul li.lang-flags img { - width: 35px; - height: 30px; - cursor: pointer; - opacity: 0.5; -} -main header nav ul li.lang-flags img.selected, main header nav ul li.lang-flags img:hover { - opacity: 1; -} -main header nav .burger { - display: none; -} -@media screen and (max-width: 560px) { - main header nav { - justify-content: space-between; - } - main header nav .burger { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - flex-direction: column; - font-weight: bold; - border: 1px solid; - margin: 0 20px; - cursor: pointer; - border-radius: 100%; - width: 35px; - height: 35px; - color: #555d61; - font-size: 25px; - } - main header nav ul { - display: none; - } - main header nav ul.responsive-show { - display: flex; - flex-direction: column; - position: absolute; - right: 0; - max-width: 100vw; - min-width: 50vw; - top: 60px; - background-color: white; - box-shadow: 0 4px 6px 2px #0000000a; - height: unset; - } - main header nav ul.responsive-show li.active a { - border: none; - } - main header nav ul.responsive-show li .submenu { - display: flex; - visibility: visible; - position: relative; - height: unset; - max-height: unset; - transition: max-height 0.6s; - top: unset; - left: unset; - margin-left: 20px; - } - main header nav ul.responsive-show li .submenu li a { - font-weight: 400; - font-size: 14px; - color: #96a5ae; - } - main header nav ul.responsive-show li.lang-flags { - margin-left: unset; - justify-content: space-around; - padding: 20px; - } -} -main #page-container { - width: 100%; - flex: 1; -} -main #page-container .page-header { - background-image: url("/assets/images/wallpaper_binary.png"); - padding: 50px 0; -} -main #page-container .page-header h1 { - padding: 15px 40px 0; - font-size: 25px; - color: #4baabb; - margin: 0 auto; -} -main #page-container .page-header p { - color: #72e3f0; - font-style: italic; - padding: 15px 40px 15px 100px; - margin: 0 auto; - font-size: 18px; -} -main #page-container .page-header p * { - color: #72e3f0; -} -main #page-container .page-header .big-logo { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - gap: 20px; - padding: 20px; -} -main #page-container .page-header .big-logo img { - width: 200px; - max-width: 100%; -} -main #page-container .page-header .big-logo img.logo-text { - width: 300px; - max-width: 100%; -} -main #page-container .page-header .logo { - padding-left: 30px; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; -} -main #page-container .page-header .logo img { - width: 100%; -} -@media screen and (max-width: 560px) { - main #page-container .page-header h1 { - padding: 15px 20px 0; - } - main #page-container .page-header p { - padding: 20px 20px 30px 40px; - text-align: justify; - } - main #page-container .page-header .big-logo { - flex-direction: column; - } -} -main #page-container .page-header.logo-left .grid-wrapper { - display: grid; - grid-template-columns: 120px 1fr; - grid-template-rows: auto 1fr; -} -main #page-container .page-header.logo-left .grid-wrapper h1 { - width: 100%; -} -main #page-container .page-header.logo-left .grid-wrapper .logo { - grid-column: 1; - grid-row: 1; - width: 100%; -} -main #page-container .page-header.logo-left .grid-wrapper p { - margin: 0; - grid-column: 1/span 2; -} -@media screen and (max-width: 780px) { - main #page-container .page-header.logo-left .grid-wrapper h1 { - padding: 0 20px; - } - main #page-container .page-header.logo-left .grid-wrapper .logo { - padding: 0 20px; - } -} -main #page-container .page-philo { - background-image: url("/assets/images/wallpaper_binary.png"); - padding: 120px 30px; -} -main #page-container .page-philo p { - width: 100%; - max-width: 600px; - font-size: 18px; - color: #aabbc8; - text-align: center; - font-style: italic; - font-weight: bold; -} -main #page-container .page-philo p * { - color: #aabbc8; -} -main #page-container .page-contents-center { - width: 1300px; - max-width: 100%; - margin: 0 auto; -} -@media screen and (max-width: 1300px) { - main #page-container .page-contents-center { - padding: 20px 20px 0; - } -} -main #page-container h2.page-section-title { - color: #4baabb; - padding: 20px 0 10px; - width: 1300px; - max-width: 100%; - margin: 0 auto; -} -@media screen and (max-width: 1300px) { - main #page-container h2.page-section-title { - padding: 20px 20px 0; - } -} -main #page-container .article-details { - grid-column: 1/span 2; -} -main #page-container .article-details h2 { - color: #6b7880; - margin: 0 10px; - padding: 10px 0 0; - font-size: 16px; -} -main #page-container .article-details ul.details-list { - margin: 10px; -} -main #page-container .article-details ul.details-list .detail { - display: grid; - grid-template-columns: 1fr auto; - gap: 20px; - font-size: 12px; - border-bottom: 1px solid #d4d9dd; - padding: 5px 0; -} -main #page-container .article-details ul.details-list .detail label { - font-weight: bold; - color: #6b7880; -} -main #page-container .article-details ul.details-list .detail .detail-value { - text-align: right; -} -main #page-container #home-page { - display: flex; - flex-direction: column; -} -main #page-container #home-page .section-title { - padding: 10px; - margin: 0; - color: #aabbc8; -} -main #page-container #home-page .page-header .philo-bubbles { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - flex-wrap: wrap; - gap: 40px; - margin: 30px 20px; -} -@media screen and (max-width: 780px) { - main #page-container #home-page .page-header .philo-bubbles { - gap: 20px; - } -} -main #page-container #home-page .page-header .philo-bubbles li { - border-radius: 100%; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - background-color: #d4d9dd; - width: 100px; - height: 100px; -} -main #page-container #home-page .page-header .philo-bubbles li * { - color: #6b7880; -} -@media screen and (max-width: 560px) { - main #page-container #home-page .page-header .philo-bubbles li { - width: 75px; - height: 75px; - } - main #page-container #home-page .page-header .philo-bubbles li * { - font-size: 12px; - } -} -main #page-container #home-page .page-header .philo-bubbles li:first-child { - background-color: #6b7880; -} -main #page-container #home-page .page-header .philo-bubbles li:first-child * { - color: white; -} -main #page-container #home-page .page-header .philo-bubbles li:last-child { - background-color: #35393c; -} -main #page-container #home-page .page-header .philo-bubbles li:last-child * { - color: #96a5ae; -} -main #page-container #home-page .poles { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 30px; - padding: 100px 0; -} -main #page-container #home-page .poles .theme-card { - display: flex; - flex-direction: column; - width: 100%; - cursor: pointer; - transition: transform 0.3s; -} -main #page-container #home-page .poles .theme-card .card-img { - width: 100%; - height: 240px; - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - flex-direction: column; - position: relative; -} -main #page-container #home-page .poles .theme-card .card-img img { - position: absolute; - max-width: 100%; - height: 100%; - padding: 10px; -} -main #page-container #home-page .poles .theme-card .card-title h2 { - margin: 0; - text-align: center; - padding: 10px 20px; - color: #4baabb; - display: block; - background-color: white; -} -main #page-container #home-page .poles .theme-card .card-description { - flex: 1; - padding: 30px 20px; -} -main #page-container #home-page .poles .theme-card .card-description p { - margin: 0; - color: #4baabb; - text-align: center; -} -main #page-container #home-page .poles .theme-card:hover { - transform: scale(1.03); -} -main #page-container #home-page .kuadrado-values { - background-image: url("/assets/images/wallpaper_binary_light.png"); - padding: 100px 0 120px; -} -main #page-container #home-page .kuadrado-values *:not(a, blue) { - color: #d4d9dd; -} -main #page-container #home-page .kuadrado-values h2 { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - margin: 0 auto 60px; - width: 120px; - height: 120px; - background-image: url("/assets/images/wallpaper_binary.png"); - border-radius: 100%; - color: #72e3f0; -} -main #page-container #home-page .kuadrado-values ul.values-list { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 30px; -} -main #page-container #home-page .kuadrado-values ul.values-list li { - background-image: url("/assets/images/wallpaper_binary.png"); - padding: 30px 20px 40px; -} -main #page-container #home-page .kuadrado-values ul.values-list li h3 { - text-align: center; -} -main #page-container #home-page .kuadrado-values ul.values-list li p { - text-align: justify; -} -@media screen and (max-width: 900px) { - main #page-container #home-page .poles { - grid-template-columns: 1fr; - gap: 40px; - } - main #page-container #home-page .poles .theme-card { - transition: transform 0.3s; - } - main #page-container #home-page .poles .theme-card .card-img { - height: 300px; - } - main #page-container #home-page .poles .theme-card .card-img img { - min-width: unset; - height: 100%; - } - main #page-container #home-page .poles .theme-card .card-title h2 { - padding: 5px 20px; - } - main #page-container #home-page .poles .theme-card .card-description { - padding: 20px 30px; - } - main #page-container #home-page .poles .theme-card:hover { - transform: none; - } - main #page-container #home-page .kuadrado-values ul.values-list { - grid-template-columns: 1fr; - } -} -@media screen and (max-width: 1300px) { - main #page-container #home-page .poles { - padding: 20px; - } - main #page-container #home-page .articles-displayer { - padding: 0; - } -} -main #page-container #education-page h3.big { - font-size: 30px; -} -main #page-container #education-page .title-banner { - display: flex; - justify-content: flex-end; - flex-direction: column; - height: 20vw; - min-height: 250px; - background-image: url("/assets/images/popularization_banner.png"); - background-size: cover; - background-repeat: no-repeat; - background-position: center; -} -main #page-container #education-page .title-banner h2 { - color: white; - font-size: 2.5em; - margin: 40px; - text-shadow: 0 0 6px #0003; -} -main #page-container #education-page .special-announcement { - background-color: #ffd000; -} -main #page-container #education-page .special-announcement .page-contents-center { - padding: 0 20px; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; -} -main #page-container #education-page .special-announcement .page-contents-center p { - color: #555d61; - font-size: 20px; - font-weight: 600; - margin: 0; - padding: 40px 0; -} -main #page-container #education-page .edu-themes { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 50px; - font-family: monospace; - padding: 70px 0; -} -main #page-container #education-page .edu-themes .edu-theme { - display: grid; - grid-template-columns: auto 1fr; -} -main #page-container #education-page .edu-themes .edu-theme * { - border-style: dashed; - border-color: #00ff00; - border-width: 0 0 0 0; -} -main #page-container #education-page .edu-themes .edu-theme h3 { - color: #00ff00; - grid-row: 1; - margin: 0; - padding: 10px; - display: flex; - align-items: center; - border-width: 0 0 0 1px; -} -main #page-container #education-page .edu-themes .edu-theme img { - width: 100%; - grid-row: 1/span 2; - border-width: 1px 0 1px 1px; -} -main #page-container #education-page .edu-themes .edu-theme p { - text-align: justify; - color: #72e3f0; - grid-row: 2; - margin: 0; - padding: 10px 30px 0 10px; - border-width: 1px 1px 1px 0; -} -main #page-container #education-page .edu-themes .edu-theme p * { - color: #72e3f0; -} -main #page-container #education-page .practical-info { - padding: 50px 0; -} -main #page-container #education-page .practical-info .page-contents-center { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 50px; -} -main #page-container #education-page .practical-info .page-contents-center .info-block { - display: grid; - grid-template-rows: auto 1fr; -} -main #page-container #education-page .practical-info .page-contents-center .info-block .info-title { - color: #4baabb; - margin: 0; - border-bottom: 1px dashed #aabbc8; - border-left: 1px dashed #aabbc8; - padding: 10px; -} -main #page-container #education-page .practical-info .page-contents-center .info-block .info-body { - margin: 0; - padding: 20px 10px; - border-right: 1px dashed #aabbc8; - border-bottom: 1px dashed #aabbc8; -} -main #page-container #education-page .practical-info .page-contents-center .info-block ul { - display: flex; - flex-direction: column; - gap: 5px; -} -main #page-container #education-page .practical-info .page-contents-center .info-block ul li:not(.fullwidth) { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; -} -main #page-container #education-page .practical-info .page-contents-center .info-block ul.tabled li span { - padding: 3px 0; -} -main #page-container #education-page .practical-info .page-contents-center .info-block ul.tabled li span:first-child { - font-weight: bold; - color: #6b7880; -} -main #page-container #education-page .practical-info .page-contents-center .info-block ul.tabled li span:last-child { - color: #4baabb; -} -@media screen and (max-width: 1200px) { - main #page-container #education-page .edu-themes { - grid-template-columns: 1fr; - gap: 30px; - padding: 70px 0; - } -} -@media screen and (max-width: 780px) { - main #page-container #education-page .practical-info .page-contents-center { - grid-template-columns: 1fr; - gap: 30px; - } - main #page-container #education-page .practical-info .page-contents-center .info-block .info-title { - border-top: 1px dashed #aabbc8; - } - main #page-container #education-page .practical-info .page-contents-center .info-block .info-body { - border-bottom: none; - } -} -@media screen and (max-width: 560px) { - main #page-container #education-page .edu-themes .edu-theme h3 { - border-width: 0 0 1px 1px; - } - main #page-container #education-page .edu-themes .edu-theme img { - max-width: 150px; - height: auto; - grid-row: 1; - border-width: 1px 0 0 1px; - } - main #page-container #education-page .edu-themes .edu-theme p { - grid-row: 2; - grid-column: 1/span 2; - padding: 20px 10px 30px 10px; - border-width: 0 1px 1px 1px; - } -} -main #page-container #games-page .game-articles article { - display: grid; - grid-template-columns: 0.7fr 1fr; - gap: 30px 50px; - margin: 20px 0; -} -main #page-container #games-page .game-articles article.game-article { - grid-template-rows: repeat(7, auto); - width: 100%; -} -main #page-container #games-page .game-articles article.game-article .game-title { - grid-column: 1/span 2; - margin: 0; - padding: 30px 20px; - color: #aabbc8; - font-size: 35px; - font-style: italic; -} -main #page-container #games-page .game-articles article.game-article .game-banner { - grid-column: 1/span 2; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - flex-direction: column; - background-color: black; - overflow: hidden; - height: 300px; -} -main #page-container #games-page .game-articles article.game-article .game-banner img { - width: 100%; -} -main #page-container #games-page .game-articles article.game-article .game-tags { - display: flex; - gap: 10px; - flex-wrap: wrap; - margin: 10px 20px; -} -main #page-container #games-page .game-articles article.game-article .game-tags span { - font-size: 12px; - padding: 4px; - background-color: #d4d9dd; - color: #6b7880; - border-radius: 5px; - font-weight: 600; -} -main #page-container #games-page .game-articles article.game-article .game-subtitle { - grid-column: 1; - margin: 10px 20px; - color: #6b7880; -} -main #page-container #games-page .game-articles article.game-article .game-description { - grid-column: 1; - text-align: justify; - margin: 10px 20px 30px; -} -main #page-container #games-page .game-articles article.game-article .image-carousel { - grid-column: 2; - grid-row: 3/span 4; - height: 400px; -} -main #page-container #games-page .game-articles article.placeholder { - height: 400px; -} -main #page-container #games-page .game-articles article.placeholder * { - background-color: #d4d9dd; -} -@media screen and (max-width: 900px) { - main #page-container #games-page .game-articles article { - grid-template-columns: 1fr; - } - main #page-container #games-page .game-articles article.game-article { - grid-template-rows: repeat(6, auto); - } - main #page-container #games-page .game-articles article.game-article .game-title { - grid-column: 1; - padding: 0; - font-size: 25px; - } - main #page-container #games-page .game-articles article.game-article .game-banner { - grid-column: 1; - margin: 0 -20px; - height: 200px; - } - main #page-container #games-page .game-articles article.game-article .image-carousel { - grid-column: 1; - grid-row: 3; - margin: 0 -20px; - } -} -main #page-container #games-page .game-articles article .play-button { - border: none; - background-color: unset; - font-weight: bold; - font-size: 20px; - cursor: pointer; - color: #4baabb; -} -main #page-container #games-page .game-articles article .play-button:hover { - color: #72e3f0; -} -main #page-container #software-page .software-articles { - margin: 20px auto 50px; -} -main #page-container #software-page .software-articles article.software-article { - display: grid; - grid-template-columns: auto 1fr; - margin: 0 0 50px; - gap: 10px 30px; -} -main #page-container #software-page .software-articles article.software-article .software-title { - grid-column: 2; - color: #aabbc8; - margin: 0; - padding: 10px; -} -main #page-container #software-page .software-articles article.software-article .software-subtitle { - grid-column: 2; - margin: 10px; - color: #6b7880; -} -main #page-container #software-page .software-articles article.software-article .software-description { - grid-column: 2; - text-align: justify; - margin: 10px; -} -main #page-container #software-page .software-articles article.software-article .software-image { - padding: 20px; - background-color: black; - grid-column: 1; - grid-row: 1/span 3; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - width: 200px; - height: 200px; - overflow: hidden; - border-radius: 100%; -} -main #page-container #software-page .software-articles article.software-article .software-image img { - max-width: 100%; - max-height: 400px; -} -@media screen and (max-width: 900px) { - main #page-container #software-page .software-articles article.software-article .software-title { - display: flex; - align-items: center; - } - main #page-container #software-page .software-articles article.software-article .software-subtitle, -main #page-container #software-page .software-articles article.software-article .software-description { - grid-column: 1/span 2; - } - main #page-container #software-page .software-articles article.software-article .software-image { - width: 100px; - height: 100px; - grid-row: 1; - } -} -main #page-container #software-page .software-articles article.placeholder { - display: flex; - flex-direction: column; - gap: 10px; - margin: 30px; -} -main #page-container #software-page .software-articles article.placeholder * { - background-color: #d4d9dd; -} -main #page-container #software-page .software-articles article.placeholder .title { - height: 60px; -} -main #page-container #software-page .software-articles article.placeholder .body { - height: 400px; -} -main #page-container #software-page .software-articles article.placeholder .details { - height: 200px; -} -main footer { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - flex-direction: column; - width: 100%; - background-image: url("/assets/images/wallpaper_binary.png"); - padding: 40px 20px; - gap: 20px; - font-size: 12px; -} -main footer span { - color: #96a5ae; - text-align: center; -} -main footer .logo { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - gap: 10px; -} -main footer .logo img { - width: 35px; -} -main footer .logo img.text-logo { - width: 100px; -} -main footer .social { - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - gap: 20px; -} -main footer .social a { - background-color: #555d61; - display: flex; - justify-content: center; - align-items: center; - flex-direction: row; - width: 25px; - height: 25px; - font-weight: bold; - font-size: 16px; - border-radius: 100%; -} - -/*# sourceMappingURL=style.css.map */ diff --git a/website/package.json b/website/package.json index e40a07a033d61aa39118cc1e532cbe831585b64f..89c51cf5432422b651fc5d25c82ceb6a0625703d 100644 --- a/website/package.json +++ b/website/package.json @@ -5,9 +5,9 @@ "main": "src/main.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "style-watch": "sass --watch ./src/style.scss ./public/style/style.css", + "style-watch": "sass --watch ./src/style.scss ../public/style/style.css", "build": "node build.js && sass ./src/style.scss ../public/style/style.css", - "build-prod": "node build.js prod && sass ./src/style.scss ./public/style/style.css --style=compressed" + "build-prod": "node build.js prod && sass ./src/style.scss ../public/style/style.css --style=compressed" }, "repository": "https://gitlab.com/peter_rabbit/kuadrado-website", "author": "Kuadrado", @@ -22,4 +22,4 @@ "sass": "^1.32.0", "simple-browser-js-bundler": "^0.1.1" } -} +} \ No newline at end of file diff --git a/website/src/pages/education/components/edu-article.js b/website/src/pages/education/components/edu-article.js new file mode 100644 index 0000000000000000000000000000000000000000..ab035167a8d5f31ec639d9b53731ba04cb2a4df3 --- /dev/null +++ b/website/src/pages/education/components/edu-article.js @@ -0,0 +1,84 @@ +"use strict"; + +const { images_url } = require("../../../../constants"); +const ImageCarousel = require("../../../generic-components/image-carousel"); +const { getArticleBody } = require("../../../lib/article-utils"); + +class EduArticle { + constructor(props) { + this.props = props; + } + + render() { + const { title, body, subtitle, images, details = [] } = this.props; + + return { + tag: "article", + class: "edu-article", + typeof: "Article", + contents: [ + { + tag: "h2", + class: "edu-art-title", + contents: title, + property: "name", + }, + { + tag: "div", class: "edu-art-image", + contents: [ + { + tag: "img", src: `${images_url}/${images[0]}` + } + ] + }, + { + tag: "h3", + class: "edu-art-subtitle", + contents: subtitle, + property: "alternativeHeadline", + }, + { + tag: "div", + class: "edu-art-description", + contents: getArticleBody(body), + property: "description", + }, + images.length > 1 && { + tag: "div", class: "edu-art-carousel", contents: [ + new ImageCarousel({ images: images.map(img => `${images_url}/${img}`) }).render() + ] + }, + details.length > 0 && { + tag: "div", + class: "article-details edu-art-details", + contents: [ + { + tag: "h2", + contents: "Details", + }, + { + tag: "ul", + class: "details-list", + contents: details.map(detail => { + return { + tag: "li", + class: "detail", + contents: [ + { tag: "label", contents: detail.label }, + { + tag: "div", + class: "detail-value", + contents: detail.value + }, + ], + }; + }), + }, + ], + }, + ], + }; + } +} + +module.exports = EduArticle; \ No newline at end of file diff --git a/website/src/pages/education/components/edu-articles.js b/website/src/pages/education/components/edu-articles.js new file mode 100644 index 0000000000000000000000000000000000000000..0c2ba4ede0f4ef12ec8349eea87a9538f085580b --- /dev/null +++ b/website/src/pages/education/components/edu-articles.js @@ -0,0 +1,60 @@ +"use strict"; +const { loadArticles } = require("../../../lib/article-utils"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); +const EduArticle = require("./edu-article"); + +class EduArticles { + constructor(props) { + this.props = props; + this.state = { + articles: [], + loaded: false, + }; + this.id = "edu-articles-section"; + this.loadArticles(); + } + + loadArticles() { + loadArticles("education", translator.locale) + .then(articles => { + this.state.articles = articles; + }) + .catch(e => console.log(e)) + .finally(() => { + this.state.loaded = true; + this.refresh() + }); + } + + renderPlaceholder() { + return { + tag: "article", + class: "placeholder", + contents: [{ tag: "div" }, { tag: "div" }, { tag: "div" }, { tag: "div" }], + }; + } + + refresh() { + obj2htm.subRender(this.render(), document.getElementById(this.id), { + mode: "replace", + }); + } + + render() { + const { articles, loaded } = this.state; + return { + tag: "section", + class: "edu-articles page-contents-center", + id: this.id, + contents: + loaded && articles.length > 0 + ? articles.map(article => new EduArticle({ ...article }).render()) + : loaded && articles.length === 0 ? [ + { tag: "p", contents: t("Rien de prévu pour le moment") } + ] : [this.renderPlaceholder()], + }; + } +} + +module.exports = EduArticles; \ No newline at end of file diff --git a/website/src/pages/education/education.js b/website/src/pages/education/education.js index efd67647628720b8b56333c6fda7f27620280728..ea04c92fc6a77ecb5ca8fae6357eb96702481daa 100644 --- a/website/src/pages/education/education.js +++ b/website/src/pages/education/education.js @@ -3,6 +3,7 @@ const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); const translator = require("ks-cheap-translator"); +const EduArticles = require("./components/edu-articles"); const t = translator.trad.bind(translator); const EDU_THEMES = [ @@ -79,7 +80,6 @@ class EducationPage extends WebPage { }, { tag: "section", - class: "bg-dark", contents: [ { tag: "div", @@ -104,55 +104,8 @@ class EducationPage extends WebPage { }, ] }, - { - tag: "section", - class: "practical-info", - contents: [ - { - tag: "div", - class: "page-contents-center", - contents: [ - { - tag: "div", - class: "info-block", - contents: [ - { tag: "h3", class: "info-title", contents: `${t("Pour s'inscrire ou en savoir plus")} <em>(programme 2022 à définir, plus d'infos bientôt)</em>` }, - { - tag: "ul", - class: "info-body", - contents: [ - { - tag: "li", - contents: [ - { tag: "span", contents: t("Me contacter") }, - { - tag: "a", - href: "mailto:contact@kuadrado-software.fr", - contents: "contact@kuadrado-software.fr", - } - ] - }, - { - tag: "li", - contents: [ - { tag: "span", contents: "" }, - { - tag: "a", - href: "tel:+33475780872", - contents: "04 75 78 08 72", - property: "telephone", - }, - ] - }, - ] - } - ] - } - ] - } - ] - - }, + { tag: "h2", class: "edu-section-title page-contents-center", contents: t("Programme XXXX", { date: "2022" }) }, + new EduArticles().render(), ], }; } diff --git a/website/src/pages/education/education.scss b/website/src/pages/education/education.scss index d284dfc40d3e47226193841d2862e08ecfb48887..68712327924f4c950f1560fd1bde6bbffbed7202 100644 --- a/website/src/pages/education/education.scss +++ b/website/src/pages/education/education.scss @@ -1,177 +1,247 @@ #education-page { - h3 { - &.big { - font-size: 30px; - } - } - .title-banner { - display: flex; - justify-content: flex-end; - flex-direction: column; - height: 20vw; - min-height: 250px; - background-image: url("/assets/images/popularization_banner.png"); - background-size: cover; - background-repeat: no-repeat; - background-position: center; - h2 { - color: white; - font-size: 2.5em; - margin: 40px; - text-shadow: 0 0 6px #0003; - } - } - .special-announcement { - background-color: $yellow_2; - .page-contents-center { - padding: 0 20px; - @include flex-center; - p { - color: $dark_3; - font-size: 20px; - font-weight: 600; - margin: 0; - padding: 40px 0; - } - } - } - - .edu-themes { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 50px; - font-family: monospace; - padding: 70px 0; - .edu-theme { - display: grid; - grid-template-columns: auto 1fr; - * { - border-style: dashed; - border-color: $green; - border-width: 0 0 0 0; - } - h3 { - color: $green; - grid-row: 1; - margin: 0; - padding: 10px; - display: flex; - align-items: center; - border-width: 0 0 0 1px; - } - img { - width: 100%; - grid-row: 1 / span 2; - border-width: 1px 0 1px 1px; - } - p { - text-align: justify; - color: $blue_3; - * { - color: $blue_3; - } - grid-row: 2; - margin: 0; - padding: 10px 30px 0 10px; - border-width: 1px 1px 1px 0; - } - } - } - - .practical-info { - padding: 50px 0; - .page-contents-center { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 50px; - .info-block { - display: grid; - grid-template-rows: auto 1fr; - .info-title { - color: $blue_2; - margin: 0; - border-bottom: 1px dashed $light_2; - border-left: 1px dashed $light_2; - padding: 10px; - } - .info-body { - margin: 0; - padding: 20px 10px; - border-right: 1px dashed $light_2; - border-bottom: 1px dashed $light_2; - } - ul { - display: flex; - flex-direction: column; - gap: 5px; - li:not(.fullwidth) { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - } - &.tabled { - li { - span { - padding: 3px 0; - &:first-child { - font-weight: bold; - color: $medium_grey; - } - &:last-child { - color: $blue_2; - } - } - } - } - } - } - } - } - - @media screen and (max-width: 1200px) { - .edu-themes { - grid-template-columns: 1fr; - gap: 30px; - padding: 70px 0; - } - } - - @media screen and (max-width: $screen_m) { - .practical-info { - .page-contents-center { - grid-template-columns: 1fr; - gap: 30px; - .info-block { - .info-title { - border-top: 1px dashed $light_2; - } - .info-body { - border-bottom: none; - } - } - } - } - } - - @media screen and (max-width: $screen_s) { - .edu-themes { - .edu-theme { - h3 { - border-width: 0 0 1px 1px; - } - img { - max-width: 150px; - height: auto; - grid-row: 1; - border-width: 1px 0 0 1px; - } - p { - grid-row: 2; - grid-column: 1 / span 2; - padding: 20px 10px 30px 10px; - border-width: 0 1px 1px 1px; - } - } - } - } + h3 { + &.big { + font-size: 30px; + } + } + .title-banner { + display: flex; + justify-content: flex-end; + flex-direction: column; + height: 20vw; + min-height: 250px; + background-image: url("/assets/images/popularization_banner.png"); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + h2 { + color: white; + font-size: 2.5em; + margin: 40px; + text-shadow: 0 0 6px #0003; + } + } + .special-announcement { + background-color: $yellow_2; + .page-contents-center { + padding: 0 20px; + @include flex-center; + p { + color: $dark_3; + font-size: 20px; + font-weight: 600; + margin: 0; + padding: 40px 0; + } + } + } + + .edu-section-title { + color: $blue_2; + margin: 40px auto; + display: flex; + align-items: center; + white-space: nowrap; + gap: 15px; + &::after { + content: ""; + display: block; + flex: 1; + border: 1px solid; + } + } + + .edu-themes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 50px; + font-family: monospace; + padding: 40px 0; + .edu-theme { + display: grid; + grid-template-columns: auto 1fr; + h3 { + color: $blue_2; + grid-row: 1; + margin: 0; + padding: 10px; + display: flex; + align-items: center; + } + img { + width: 100%; + grid-row: 1 / span 2; + } + p { + text-align: justify; + grid-row: 2; + margin: 0; + padding: 10px 30px 0 10px; + } + } + } + + .edu-articles { + display: flex; + flex-direction: column; + gap: 40px; + margin: 20px auto; + article.edu-article { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px 30px; + ul { + list-style-type: unset; + margin-left: 30px; + } + + .edu-art-image { + padding: 20px; + background-color: black; + grid-column: 1; + grid-row: 1 / span 3; + @include flex-center; + width: 150px; + height: 150px; + overflow: hidden; + border-radius: 100%; + img { + max-width: 100%; + max-height: 400px; + } + } + + .edu-art-title { + grid-column: 2 / span 3; + color: $light_2; + margin: 0; + padding: 10px; + } + + .edu-art-subtitle { + grid-column: 2 / span 3; + margin: 10px; + color: $medium_grey; + } + + .edu-art-description { + grid-column: 2; + text-align: justify; + margin: 10px; + } + + .edu-art-carousel { + grid-column: 3; + width: 30vw; + max-width: 500px; + .image-carousel { + width: 100%; + height: 20vw; + max-height: 400px; + img { + width: 100%; + } + } + } + + .edu-art-details { + grid-column: 1 / span 3; + } + + @media screen and (max-width: 1050px) { + grid-template-columns: auto 1fr; + .edu-art-title { + grid-column: 2; + } + .edu-art-image { + grid-row: 1 / span 2; + } + .edu-art-carousel { + width: 100%; + max-width: unset; + grid-column: 1 / span 2; + grid-row: 3; + .image-carousel { + height: 40vw; + max-height: 400px; + } + } + .edu-art-subtitle { + grid-column: 2; + grid-row: 2; + } + + .edu-art-description { + grid-column: 1 / span 2; + } + + .edu-art-details { + grid-column: 1 / span 2; + } + } + + @media screen and (max-width: $screen_l) { + .edu-art-title { + display: flex; + align-items: center; + } + + .edu-art-subtitle { + grid-column: 1 / span 2; + } + .edu-art-image { + width: 100px; + height: 100px; + grid-row: 1; + } + // .edu-art-carousel { + // grid-column: 1 / span 2; + // } + } + } + + article.placeholder { + display: grid; + grid-template-columns: 200px auto; + grid-template-rows: 100px 100px 200px; + gap: 10px; + margin: 30px; + * { + background-color: $light_0; + flex: 1; + &:first-child { + // image + grid-row: 1 / span 2; + } + &:last-child { + grid-column: 1 / span 2; + } + } + } + } + + @media screen and (max-width: 1200px) { + .edu-themes { + grid-template-columns: 1fr; + gap: 30px; + padding: 0; + } + } + + @media screen and (max-width: $screen_s) { + .edu-themes { + .edu-theme { + img { + max-width: 150px; + height: auto; + grid-row: 1; + } + p { + grid-row: 2; + grid-column: 1 / span 2; + padding: 20px 10px 30px 10px; + } + } + } + } } diff --git a/website/src/style.scss b/website/src/style.scss index e3ca9c780a5ec05aec8bccf62914ffa7717c492b..d523c3b1c5cf1ad8d95e59a0db629dc2e449f9c8 100644 --- a/website/src/style.scss +++ b/website/src/style.scss @@ -413,6 +413,7 @@ main { label { font-weight: bold; color: $medium_grey; + white-space: nowrap; } .detail-value { text-align: right;