From 3c8de41ef56f169435d6ba5562a7e1f23bd155d4 Mon Sep 17 00:00:00 2001 From: Eunomiac Date: Thu, 4 Jan 2024 05:42:36 -0500 Subject: [PATCH] More progress on socket animations --- css/style.min.css | 38 +- module/classes/BladesClocks.js | 143 +++++- module/classes/BladesDirector.js | 133 ++---- module/core/gsap.js | 348 +++++++------- module/core/helpers.js | 1 - module/core/utilities.js | 117 +++-- module/sheets/item/BladesClockKeeperSheet.js | 23 +- module/sheets/item/BladesItemSheet.js | 3 - scss/components/_clocks.scss | 5 +- scss/overlay/_blades-overlay.scss | 7 + scss/sheets/_clock-keeper-sheet.scss | 12 +- templates/parts/clock-sheet-key-controls.hbs | 11 +- .../roll/partials/roll-collab-action-gm.hbs | 15 +- .../roll/partials/roll-collab-action.hbs | 17 +- .../roll/partials/roll-collab-fortune.hbs | 26 +- .../partials/roll-collab-indulgevice-gm.hbs | 443 ++++++++---------- .../roll/partials/roll-collab-indulgevice.hbs | 380 +++++++++------ ts/classes/BladesClocks.ts | 121 +++-- ts/classes/BladesDirector.ts | 141 ++---- ts/core/gsap.ts | 397 ++++++++-------- ts/core/helpers.ts | 1 - ts/core/utilities.ts | 130 +++-- ts/sheets/item/BladesClockKeeperSheet.ts | 28 +- ts/sheets/item/BladesItemSheet.ts | 3 - 24 files changed, 1410 insertions(+), 1133 deletions(-) diff --git a/css/style.min.css b/css/style.min.css index eeaa5b62..e104a253 100644 --- a/css/style.min.css +++ b/css/style.min.css @@ -6440,6 +6440,12 @@ template { perspective: 500px; transform-style: preserve-3d; } +:root body.vtt.game.system-eunos-blades #blades-overlay #test-box { + height: 200px; + width: 200px; + background: blue; + top: 200px; +} :root body.vtt.game.system-eunos-blades #blades-overlay { /* KEYS */ /* CLOCKS */ @@ -6614,8 +6620,8 @@ template { left: 0; position: absolute; } -:root body.vtt.game.system-eunos-blades #blades-overlay .clock-key-container .clock-key .key-label:hover { - color: var(--blades-gold-bright) !important; +:root body.vtt.game.system-eunos-blades #blades-overlay .clock-key-container .clock-key .key-label.label-hidden { + visibility: hidden; } :root body.vtt.game.system-eunos-blades #blades-overlay .clock-key-container .clock-key .key-image-container { height: var(--key-height); @@ -6652,7 +6658,6 @@ template { :root body.vtt.game.system-eunos-blades #blades-overlay .clock-container .clock { position: relative; overflow: visible; - pointer-events: all !important; } :root body.vtt.game.system-eunos-blades #blades-overlay .clock-container .clock, :root body.vtt.game.system-eunos-blades #blades-overlay .clock-container .clock * { height: var(--clock-size, 110px); @@ -9761,8 +9766,8 @@ template { left: 0; position: absolute; } -:root body.vtt.game.system-eunos-blades #clocks-overlay .clock-key-container .clock-key .key-label:hover { - color: var(--blades-gold-bright) !important; +:root body.vtt.game.system-eunos-blades #clocks-overlay .clock-key-container .clock-key .key-label.label-hidden { + visibility: hidden; } :root body.vtt.game.system-eunos-blades #clocks-overlay .clock-key-container .clock-key .key-image-container { height: var(--key-height); @@ -9799,7 +9804,6 @@ template { :root body.vtt.game.system-eunos-blades #clocks-overlay .clock-container .clock { position: relative; overflow: visible; - pointer-events: all !important; } :root body.vtt.game.system-eunos-blades #clocks-overlay .clock-container .clock, :root body.vtt.game.system-eunos-blades #clocks-overlay .clock-container .clock * { height: var(--clock-size, 110px); @@ -23436,8 +23440,8 @@ template { left: 0; position: absolute; } -:root body.vtt.game.system-eunos-blades .app.window-app .clock-key-container .clock-key .key-label:hover { - color: var(--blades-gold-bright) !important; +:root body.vtt.game.system-eunos-blades .app.window-app .clock-key-container .clock-key .key-label.label-hidden { + visibility: hidden; } :root body.vtt.game.system-eunos-blades .app.window-app .clock-key-container .clock-key .key-image-container { height: var(--key-height); @@ -23474,7 +23478,6 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app .clock-container .clock { position: relative; overflow: visible; - pointer-events: all !important; } :root body.vtt.game.system-eunos-blades .app.window-app .clock-container .clock, :root body.vtt.game.system-eunos-blades .app.window-app .clock-container .clock * { height: var(--clock-size, 110px); @@ -25268,8 +25271,8 @@ template { left: 0; position: absolute; } -:root body.vtt.game.system-eunos-blades .app.window-app.sheet .window-content .clock-key-container .clock-key-container .clock-key .key-label:hover { - color: var(--blades-gold-bright) !important; +:root body.vtt.game.system-eunos-blades .app.window-app.sheet .window-content .clock-key-container .clock-key-container .clock-key .key-label.label-hidden { + visibility: hidden; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet .window-content .clock-key-container .clock-key-container .clock-key .key-image-container { height: var(--key-height); @@ -25306,7 +25309,6 @@ template { :root body.vtt.game.system-eunos-blades .app.window-app.sheet .window-content .clock-key-container .clock-container .clock { position: relative; overflow: visible; - pointer-events: all !important; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet .window-content .clock-key-container .clock-container .clock, :root body.vtt.game.system-eunos-blades .app.window-app.sheet .window-content .clock-key-container .clock-container .clock * { height: var(--clock-size, 110px); @@ -26802,17 +26804,15 @@ template { display: flex; align-items: center; justify-content: stretch; - background: linear-gradient(45deg, black, rgb(40, 40, 40)); - color: white; - font-family: "Sura"; + background: linear-gradient(45deg, var(--blades-grey-bright), var(--blades-grey-dark)); font-size: 24px; - box-shadow: inset 0 0 2px 2px black; + box-shadow: inset 0 0 2px 2px var(--blades-black-dark); } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.item.clock-keeper .window-content form .sheet-root .tab-content .tab .clock-key-control-flipper > div { position: absolute; visibility: hidden; white-space: nowrap; - box-shadow: inset 0 0 2px 2px black; + box-shadow: inset 0 0 2px 2px var(--blades-black-dark); display: flex; flex-direction: row; justify-content: space-between; @@ -26824,8 +26824,8 @@ template { border-radius: 5px; } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.item.clock-keeper .window-content form .sheet-root .tab-content .tab .clock-key-control-flipper > div.controls-back { - color: black; - background: white; + color: var(--blades-black); + background: linear-gradient(45deg, var(--blades-white-bright), var(--blades-grey-bright)); } :root body.vtt.game.system-eunos-blades .app.window-app.sheet.item.gm-tracker .window-content form { --sheet-mid-height: 100px; diff --git a/module/classes/BladesClocks.js b/module/classes/BladesClocks.js index 0a7a69d3..ea7f29b1 100644 --- a/module/classes/BladesClocks.js +++ b/module/classes/BladesClocks.js @@ -10,7 +10,14 @@ class BladesClockKey extends BladesTargetLink { function registerClockKeys(doc) { if ("clocksData" in doc.system) { Object.values(doc.system.clocksData ?? {}) - .forEach((keyData) => { new BladesClockKey(keyData); }); + .forEach((keyData) => { + try { + new BladesClockKey(keyData); + } + catch (err) { + eLog.error("BladesClockKey", "[BladesClockKey.Initialize] Error initializing clock key.", err, keyData); + } + }); } } game.items.contents @@ -21,6 +28,8 @@ class BladesClockKey extends BladesTargetLink { .forEach(registerClockKeys); socketlib.system.register("pull_SocketCall", BladesClockKey.pull_SocketResponse.bind(this)); socketlib.system.register("drop_SocketCall", BladesClockKey.drop_SocketResponse.bind(this)); + socketlib.system.register("fadeInName_SocketCall", BladesClockKey.fadeInName_SocketResponse.bind(this)); + socketlib.system.register("fadeOutName_SocketCall", BladesClockKey.fadeOutName_SocketResponse.bind(this)); return loadTemplates([ "systems/eunos-blades/templates/components/clock-key.hbs", "systems/eunos-blades/templates/components/clock.hbs" @@ -164,14 +173,6 @@ class BladesClockKey extends BladesTargetLink { async getHTML() { return await renderTemplate("systems/eunos-blades/templates/components/clock-key.hbs", this); } - async appendToOverlay() { - return game.eunoblades.Director.appendToClockKeySection(await this.getHTML()); - } - async removeFromOverlay() { - delete this._hoverOverTimeline; - delete this._keySwingTimeline; - return game.eunoblades.Director.removeFromClockKeySection(this.id); - } get elem() { return $(`#${this.id}`)[0]; } @@ -184,14 +185,11 @@ class BladesClockKey extends BladesTargetLink { get containerElem$() { return this.containerElem ? $(this.containerElem) : undefined; } - get isShowingControls() { - if (!this.elem) { - return false; - } - if (!game.user.isGM) { - return false; - } - return !$(this.elem).hasClass("controls-hidden"); + get labelElem() { + return this.elem$ ? this.elem$.find(".key-label")[0] : undefined; + } + get labelElem$() { + return this.elem$ ? this.elem$.find(".key-label") : undefined; } // Initializes clock key with proper position and scale before displaying via autoAlpha async initClockKeyElem(displayMode) { @@ -373,10 +371,10 @@ class BladesClockKey extends BladesTargetLink { _keySwingTimeline; get keySwingTimeline() { if (!this.elem) { - return undefined; + throw new Error("elem is not defined for keySwingTimeline"); } if (!$(this.elem).parents("#blades-overlay").length) { - return undefined; + throw new Error("elem is not a child of #blades-overlay"); } if (!this._keySwingTimeline) { this._keySwingTimeline = U.gsap.effects.keySwing(this.elem).pause(); @@ -386,17 +384,49 @@ class BladesClockKey extends BladesTargetLink { _hoverOverTimeline; get hoverOverTimeline() { if (!this.elem) { - return undefined; + throw new Error("elem is not defined for hoverOverTimeline"); + } + if (!$(this.elem).parents("#blades-overlay").length) { + throw new Error("elem is not a child of #blades-overlay"); } if (!this._hoverOverTimeline) { - this._hoverOverTimeline = U.gsap.effects.hoverOverClockKey(this); + this._hoverOverTimeline = U.gsap.effects.hoverOverClockKey(this.elem); } return this._hoverOverTimeline; } + _nameFadeInTimeline; + get nameFadeInTimeline() { + if (!this.labelElem$) { + throw new Error("labelElem$ is not defined for nameFadeInTimeline"); + } + if (!this.elem$?.parents("#blades-overlay")?.length) { + throw new Error("elem is not a child of #blades-overlay"); + } + if (!this._nameFadeInTimeline) { + U.gsap.killTweensOf(this.labelElem$); + this._nameFadeInTimeline = U.gsap.effects.blurReveal(this.labelElem$, { + ignoreMargin: true, + duration: 1.5, + callbackScope: this, + onStart() { + this.labelElem$.removeClass("label-hidden"); + }, + onComplete() { + U.gsap.effects.textJitter(this.labelElem$); + }, + onReverseComplete() { + this.labelElem$.addClass("label-hidden"); + U.gsap.killTweensOf(this.labelElem$); + delete this._nameFadeInTimeline; + } + }).pause(); + } + return this._nameFadeInTimeline; + } // #endregion // #region > SOCKET CALLS: _SocketCall / static _SocketResponse / _Animation async drop_Animation(callback) { - await this.appendToOverlay(); + await game.eunoblades.Director.appendClockKeyToOverlay(this); U.gsap.effects.keyDrop(this.elem, { callback }); this.keySwingTimeline?.seek(0).play(); } @@ -421,7 +451,7 @@ class BladesClockKey extends BladesTargetLink { await new Promise((resolve) => { U.gsap.effects.keyPull(this.elem, { callback }).then(resolve); }); - this.removeFromOverlay(); + game.eunoblades.Director.removeClockKeyFromOverlay(this); } async pull_SocketCall() { if (!game.user.isGM) { @@ -443,6 +473,70 @@ class BladesClockKey extends BladesTargetLink { } key.pull_Animation(); } + async fadeInName_Animation(callback) { + if (!this.labelElem$) { + return; + } + if (!this.name) { + return; + } + this.nameFadeInTimeline.play(); + if (callback) { + U.gsap.delayedCall(2, callback); + } + } + async fadeInName_SocketCall() { + if (!game.user.isGM) { + return; + } + if (!this.elem) { + return; + } + if (!$(this.elem).parents("#blades-overlay").length) { + return; + } + this.fadeInName_Animation(); + socketlib.system.executeForOthers("fadeInName_SocketCall", this.id); + } + static fadeInName_SocketResponse(keyID) { + const key = game.eunoblades.ClockKeys.get(keyID); + if (!key) { + return; + } + key.fadeInName_Animation(); + } + async fadeOutName_Animation(callback) { + if (!this.labelElem$) { + return; + } + if (!this.name) { + return; + } + this.nameFadeInTimeline.reverse(); + if (callback) { + U.gsap.delayedCall(2, callback); + } + } + async fadeOutName_SocketCall() { + if (!game.user.isGM) { + return; + } + if (!this.elem) { + return; + } + if (!$(this.elem).parents("#blades-overlay").length) { + return; + } + this.fadeOutName_Animation(); + socketlib.system.executeForOthers("fadeOutName_SocketCall", this.id); + } + static fadeOutName_SocketResponse(keyID) { + const key = game.eunoblades.ClockKeys.get(keyID); + if (!key) { + return; + } + key.fadeOutName_Animation(); + } // #endregion // #endregion // #region Adding & Removing Clocks ~ @@ -518,7 +612,6 @@ class BladesClock extends BladesTargetLink { } return pKey; } - get isShowingControls() { return this.parentKey.isShowingControls; } get isNameVisible() { return U.pBool(this.data.isNameVisible); } set isNameVisible(val) { this.updateTarget("isNameVisible", U.pBool(val)); } get isVisible() { return U.pBool(this.data.isVisible); } @@ -562,7 +655,7 @@ class BladesClock extends BladesTargetLink { // #endregion // #region HTML INTERACTION ~ get elem() { - return $(`#${this.id}"`)[0]; + return $(`#${this.id}`)[0]; } get elem$() { return this.elem ? $(this.elem) : undefined; diff --git a/module/classes/BladesDirector.js b/module/classes/BladesDirector.js index bf295b8d..4c6a8739 100644 --- a/module/classes/BladesDirector.js +++ b/module/classes/BladesDirector.js @@ -37,7 +37,6 @@ class BladesDirector { if (!this._overlayContainer) { $("body.vtt").append("
"); [this._overlayContainer] = $("#blades-overlay"); - this.resetObservers(); } return this._overlayContainer; } @@ -50,21 +49,19 @@ class BladesDirector { get clockKeySection$() { return this.overlayContainer$.find(".overlay-section-clock-keys"); } - async appendToClockKeySection(elem) { - if (typeof elem === "string") { - elem = $(elem); + async appendClockKeyToOverlay(clockKey) { + const clockKeyHTML = await clockKey.getHTML(); + $(clockKeyHTML).appendTo(this.clockKeySection$); + if (!clockKey.containerElem$) { + throw new Error("ClockKey container element not found."); } - elem = $(elem).appendTo(this.clockKeySection$); - const keyID = elem.find(".clock-key").data("id"); - const key = game.eunoblades.ClockKeys.get(keyID); - await key.initClockKeyElem(); - return key.elem$; - } - removeFromClockKeySection(elem) { - if (typeof elem === "string") { - elem = $(`#${elem}`); - } - $(elem).closest(".clock-key-container").remove(); + this.activateClockListeners(clockKey.containerElem$); + return clockKey.containerElem$; + } + removeClockKeyFromOverlay(clockKey) { + delete clockKey._hoverOverTimeline; + delete clockKey._keySwingTimeline; + clockKey.containerElem$?.remove(); } get locationSection$() { return this.overlayContainer$.find(".overlay-section-location"); @@ -89,63 +86,6 @@ class BladesDirector { } get svgData() { return SVGDATA; } // #endregion - // #region OBSERVERS ~ - get ObserverData() { - return [ - // Overlay Clock Key Observer - { - id: "overlay-clock-key", - type: "pointer", - target: game.eunoblades.Director.clockKeySection$[0], - ignore: [ - ...ObserverIgnoreStrings - ], - onPress(obs) { - if (!(obs.event.currentTarget instanceof HTMLElement)) { - return; - } - const target$ = $(obs.event.currentTarget); - if (target$.hasClass("clock-key")) { - const clockKey = game.eunoblades.ClockKeys.get(target$.attr("id") ?? ""); - if (!clockKey) { - throw new Error(`ClockKey not found for ID: '${target$.attr("id") ?? ""}'`); - } - switch (obs.event.type) { - case "dblclick": { - console.log(`Double-Click on ClockKey: ${clockKey.name || clockKey.id}`); - break; - } - case "contextmenu": { - console.log(`Right-Click on ClockKey: ${clockKey.name || clockKey.id}`); - break; - } - default: { - break; - } - } - } - } - } - ]; - } - _Observers; - get Observers() { - return this._Observers ??= new Collection(this.ObserverData - .map((oVars) => { - if (!oVars.id) { - eLog.error("BladesDirector", "Observer must have an ID", oVars); - throw new Error("Observer must have an ID"); - } - return [oVars.id, Observer.create(oVars)]; - })); - } - resetObservers() { - this._Observers?.forEach((obs) => { obs.kill(); }); - this._Observers?.clear(); - delete this._Observers; - void this.Observers; // Trigger Observer regeneration within getter. - } - // #endregion get sceneKeys() { return game.eunoblades.ClockKeeper.getSceneKeys(); } renderOverlay_SocketCall() { if (!game.user.isGM) { @@ -163,30 +103,51 @@ class BladesDirector { // Display keys that are visible this.sceneKeys .filter((key) => key.isVisible) - .forEach((key) => key.drop_Animation()); + .forEach((key) => key.drop_Animation((async () => { + await game.eunoblades.Director.activateClockListeners(key.containerElem$); + if (key.isNameVisible) { + key.nameFadeInTimeline.progress(0.99).play(); + } + }))); } - async activateClockListeners() { - this.clockKeySection$.find(".clock-key-container").each((_, keyContainer) => { + async activateClockListeners(keyContainers$ = this.clockKeySection$.find(".clock-key-container")) { + keyContainers$.each((_, keyContainer) => { const keyContainer$ = $(keyContainer); const clockKey = game.eunoblades.ClockKeys.get(keyContainer$.find(".clock-key").attr("id") ?? ""); if (!clockKey) { return; } - // Enable pointer events on the container, so that the hover-over timeline can be played - keyContainer$.css("pointer-events", "auto"); + // The ".key-bg" child is actually the correct shape, so that will be our listener object. + const keyListener$ = clockKey.elem$?.find(".key-bg"); + if (!keyListener$?.[0]) { + return; + } + // Enable pointer events on the key-bg, so that the hover-over timeline can be played, + // and remove any existing listeners to avoid duplication + keyListener$.css("pointer-events", "auto"); + keyListener$.off(); // Do the same for clocks contained by the key clockKey.clocks.forEach((clock) => { - if (!clock.elem) { + if (!clock.elem$) { return; } - $(clock.elem).css("pointer-events", "auto"); + clock.elem$.css("pointer-events", "auto"); + clock.elem$.off(); }); if (game.user.isGM) { // === GM-ONLY LISTENERS === // Double-Click a Clock Key = SocketPull it, Open ClockKeeper sheet - keyContainer$.on("dblclick", async () => { + keyListener$.on("dblclick", async () => { clockKey.pull_SocketCall(); - game.eunoblades.ClockKeeper.render(true); + if (!game.eunoblades.ClockKeeper.sheet?.rendered) { + game.eunoblades.ClockKeeper.render(true); + } + }); + // Right-Click a Clock Key = Open ClockKeeper sheet. + keyListener$.on("dblclick", async () => { + if (!game.eunoblades.ClockKeeper.sheet?.rendered) { + game.eunoblades.ClockKeeper.render(true); + } }); // Mouse-Wheel a Clock = Add/Remove Segments one-by-one. // -- can UPDATE the server data immediately @@ -218,10 +179,10 @@ class BladesDirector { else { // === PLAYER-ONLY LISTENERS === // Add listeners to container for mouseenter and mouseleave, that play and reverse timeline attached to element - keyContainer$.on("mouseenter", () => { - clockKey.hoverOverTimeline?.play(); + keyListener$.on("mouseenter", () => { + clockKey.hoverOverTimeline.play(); }).on("mouseleave", () => { - clockKey.hoverOverTimeline?.reverse(); + U.reverseRepeatingTimeline(clockKey.hoverOverTimeline); }); // Now repeat this for each clock in the clock key clockKey.clocks.forEach((clock) => { @@ -233,7 +194,9 @@ class BladesDirector { clockElem$.on("mouseenter", () => { clock.hoverOverTimeline?.play(); }).on("mouseleave", () => { - clock.hoverOverTimeline?.reverse(); + if (clock.hoverOverTimeline) { + U.reverseRepeatingTimeline(clock.hoverOverTimeline); + } }); }); } diff --git a/module/core/gsap.js b/module/core/gsap.js index 8b0e1e9a..70c4804e 100644 --- a/module/core/gsap.js +++ b/module/core/gsap.js @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import U from "./utilities.js"; import C from "./constants.js"; -import BladesClockKey, { BladesClock } from "../classes/BladesClocks.js"; +import { BladesClock } from "../classes/BladesClocks.js"; // eslint-disable-next-line import/no-unresolved import { TextPlugin, Flip, Draggable as Dragger, MotionPathPlugin, SplitText, Observer, CustomEase, CustomWiggle, CustomBounce, EasePack } from "/scripts/greensock/esm/all.js"; const gsapPlugins = [ @@ -17,30 +17,7 @@ const gsapPlugins = [ EasePack ]; export const gsapEffects = { - hoverButton: { - effect: (target, config) => { - return U.gsap.timeline({ paused: true }) - .to(target, { - scale: config.scale, - ease: "power2", - duration: config.duration - }) - .fromTo(target, { - filter: config.brightness ? "brightness(1)" : undefined - }, { - color: config.color, - filter: config.brightness ? `brightness(${config.brightness})` : undefined, - duration: config.duration, - ease: "sine" - }, 0); - }, - defaults: { - color: undefined, - brightness: 1.5, - duration: 0.5, - scale: 1.25 - } - }, + // #region CLOCK KEYS keyDrop: { effect: (clockKey, config) => { const [keyContainer] = $(clockKey).closest(".clock-key-container"); @@ -70,14 +47,14 @@ export const gsapEffects = { effect: (clockKey, config) => { const [keyContainer] = $(clockKey).closest(".clock-key-container"); // Get initial scale, - const tl = U.gsap.timeline({ id: "keySwing", repeat: -1, yoyo: true, data: { labelTimes: {} } }) + const tl = U.gsap.timeline({ id: "keySwing", repeat: -1, yoyo: true }) .fromTo(keyContainer, { transformOrigin: "50% 10%", rotateZ: -config.swingAngle }, { rotateZ: config.swingAngle, ease: "sine.inOut", - duration: config.duration / 4, + duration: 0.25 * config.duration, repeat: 2, yoyo: true }) @@ -88,14 +65,22 @@ export const gsapEffects = { top: `-=${0.5 * config.yRange}`, scale: `-=${0.5 * config.scaleRange}`, ease: "sine.inOut", - duration: config.duration - }, 0); - // Add labels at the points where rotateZ is 0, and log these times to a snappable object literal in the data property - const timesWhenRotateZIsZero = Array(4).fill(config.duration / 8).map((val, index) => val * (2 * (index + 1))); + duration: 0.75 * config.duration + }, 0.25 * config.duration); + // Get times where rotateZ is 0 + const timesWhenRotateZIsZero = Array(4).fill(config.duration / 8) + .map((val, index) => val * (2 * (index + 1))); + // Get time when top & scale shifts are 0 + const timeWhenShiftsAreZero = ((0.75 * config.duration) / 2) + (0.25 * config.duration); + // Add labels to all rotateZ === 0 times, but if shifts are also zero, name it "NEUTRAL" timesWhenRotateZIsZero.forEach((timestamp, i) => { tl.addLabel(`rotateZZero${i}`, timestamp); - tl.data.labelTimes[timestamp] = `rotateZZero${i}`; + if (timestamp === timeWhenShiftsAreZero && tl.labels.NEUTRAL === undefined) { + tl.addLabel("NEUTRAL", timestamp); + } }); + // Immediately move the timeline to the "NEUTRAL" label, so the timeline begins from there + tl.seek("NEUTRAL"); return tl; }, defaults: { @@ -149,7 +134,21 @@ export const gsapEffects = { }, extendTimeline: true }, - keyControlZoom: { + keyNameFadeIn: { + effect: (target, config) => { + return U.gsap.effects.blurReveal(target, config); + }, + defaults: { + ignoreMargin: true, + skewX: -20, + duration: 0.5, + x: "+=300", + scale: 1.5, + filter: "blur(10px)" + }, + extendTimeline: true + }, + hoverOverClockKey: { effect: (clockKeyElem, config) => { if (!clockKeyElem) { throw new Error("clockKeyElem is null or undefined"); @@ -158,72 +157,58 @@ export const gsapEffects = { if (!clockKey) { throw new Error("clockKey is null or undefined"); } - if (!clockKey.containerElem) { - throw new Error("clockKey.containerElem is null or undefined"); + if (!clockKey.elem$) { + throw new Error("clockKey.elem$ is null or undefined"); } - const tl = U.gsap.timeline({ - id: "keyZoom", - paused: true, - onStart() { - // Get the keySwing timeline, if there is one - const keySwingTimeline = clockKey.keySwingTimeline; - if (keySwingTimeline) { - // Get the current time and duration of the timeline - const currentTime = keySwingTimeline.time(); - const duration = keySwingTimeline.duration(); - // Snap to the nearest label time - const nearestLabelTime = U.gsap.utils.snap(Object.keys(keySwingTimeline.data.labelTimes).map(U.pInt), currentTime); - // Get associated label - const nearestLabel = keySwingTimeline.data.labelTimes[nearestLabelTime]; - // Animate to the nearest label, then seek to the midpoint of the animation, where scale and vertical offsets are also zero. - keySwingTimeline.tweenTo(nearestLabel, { duration: 0.25, ease: "none" }).then(() => keySwingTimeline.seek(duration / 2).pause()); - } - }, - onReverseComplete() { - clockKey.keySwingTimeline?.resume(); - } - }).to(clockKey.containerElem, { - scale: config.scale, - ease: config.ease, - duration: config.duration, - onStart() { - clockKey.containerElem$?.removeClass("controls-hidden"); - }, - onReverseComplete() { - clockKey.containerElem$?.addClass("controls-hidden"); + if (!clockKey.containerElem$) { + throw new Error("clockKey.containerElem$ is null or undefined"); + } + const clockKeyHiddenLabel$ = clockKey.elem$.find(".key-label.hidden-label"); + const clockKeyLabel$ = clockKey.elem$.find(".key-label"); + // Construct master timeline, + const tl = U.gsap.timeline({ paused: true }); + // Create initial tween that resets keySwing to neutral + tl.add(clockKey.keySwingTimeline + .tweenTo("NEUTRAL", { + duration: 0.25 * config.duration, + ease: "none" + })); + // Add a label for the proper start of the hover-over animation + tl.addLabel("hoverStart"); + // Add an initial callback that resumes keySwing if the timeline hits this point while reversed + tl.add(() => { + if (tl.reversed()) { + // Immediately seek to the beginning, so keySwing is reset on another hover-over + tl.seek(0).pause(); + clockKey.keySwingTimeline.seek("NEUTRAL").play(); } }); + // === HOVER-OVER ANIMATION === + // Brighten & enlarge clockKey + tl.fromTo(clockKeyElem, { + filter: "brightness(1)" + }, { + filter: `brightness(${config.brightness})`, + scale: function (i, target) { + return U.gsap.getProperty(target, "scale") * config.scaleMult; + }, + duration: 0.75 * config.duration + }, "hoverStart"); + // Fade in name + tl.blurReveal(clockKeyHiddenLabel$, { + ignoreMargin: true, + duration: 0.75 * config.duration + }, "hoverStart"); + // Move into repeating jitter tween + tl.textJitter(clockKeyLabel$); return tl; }, defaults: { - scale: 1.5, - ease: "sine", - duration: 1 + duration: 1.5, + brightness: 1.5, + scaleMult: 1.25 } }, - hoverOverClockKey: { - effect: (clockKey, config) => { - if (!(clockKey instanceof BladesClockKey)) { - throw new Error("clockKey is not an instance of BladesClockKey"); - } - if (!clockKey.elem) { - throw new Error("clockKey.elem is null or undefined"); - } - return U.gsap.timeline({ paused: true }) - .to(clockKey.elem, { - scale: 1.25, - ease: "sine", - duration: 0.25, - onStart() { - clockKey.keySwingTimeline?.tweenTo(1, { duration: 0.25, ease: "none" }); - }, - onReverse() { - clockKey.keySwingTimeline?.resume(); - } - }, 0); - }, - defaults: {} - }, hoverOverClock: { effect: (clock, config) => { if (!(clock instanceof BladesClock)) { @@ -253,6 +238,8 @@ export const gsapEffects = { duration: 0.5 } }, + // #endregion + // #region CHAT CONSEQUENCE EFFECTS csqEnter: { effect: (csqContainer, config) => { const csqRoot = U.gsap.utils.selector(csqContainer); @@ -673,6 +660,38 @@ export const gsapEffects = { }, defaults: {} }, + // #endregion + // #region CHARACTER SHEET EFFECTS + fillCoins: { + effect: (targets, config) => { + // Targets will be all coins from zero to where fill currently is + // Some will already be full, others not. + // Stagger in timeline + // Pulse in size and color + // Shimmer as they shrink back ? + return U.gsap.to(targets, { + duration: config.duration / 2, + scale: config.scale, + filter: config.filter, + ease: config.ease, + stagger: { + amount: 0.25, + from: "start", + repeat: 1, + yoyo: true + } + }); + }, + defaults: { + duration: 1, + scale: 1, + filter: "saturate(1) brightness(2)", + ease: "power2.in" + }, + extendTimeline: true + }, + // #endregion + // #region GENERAL: 'blurRemove', 'hoverTooltip', 'textJitter' blurRemove: { effect: (targets, config) => U.gsap.timeline() .to(targets, { @@ -682,12 +701,16 @@ export const gsapEffects = { }) .to(targets, { x: config.x, - marginBottom(i, target) { - return U.get(target, "height") * -1; - }, - marginRight(i, target) { - return U.get(target, "width") * -1; - }, + marginBottom: config.ignoreMargin + ? undefined + : function (i, target) { + return U.get(target, "height") * -1; + }, + marginRight: config.ignoreMargin + ? undefined + : function (i, target) { + return U.get(target, "width") * -1; + }, scale: config.scale, filter: config.filter, duration: (3 / 4) * config.duration @@ -698,84 +721,28 @@ export const gsapEffects = { ease: "power3.in" }, config.duration / 2), defaults: { + ignoreMargin: false, skewX: -20, duration: 0.5, x: "+=300", scale: 1.5, filter: "blur(10px)" - } - }, - slideUp: { - effect: (targets) => U.gsap.to(targets, { - height: 0, - // PaddingTop: 0, - // paddingBottom: 0, - duration: 0.5, - ease: "power3" - }), - defaults: {} - }, - pulse: { - effect: (targets, config) => U.gsap.to(targets, { - repeat: config.repCount, - yoyo: true, - duration: config.duration / config.repCount, - ease: config.ease, - opacity: 0.25 - }), - defaults: { - repCount: 3, - duration: 5, - ease: "sine.inOut" - } - }, - throb: { - effect: (targets, config) => U.gsap.to(targets, { - repeat: config.stagger ? undefined : 1, - yoyo: config.stagger ? undefined : true, - duration: config.duration / 2, - scale: config.scale, - filter: config.filter, - ease: config.ease, - stagger: config.stagger - ? { - ...config.stagger, - repeat: 1, - yoyo: true - } - : {} - }), - defaults: { - duration: 1, - scale: 1, - filter: "saturate(1) brightness(2)", - ease: "power2.in" }, extendTimeline: true }, - pulseClockWedges: { - effect: () => U.gsap.timeline({ duration: 0 }), - defaults: {} - }, - reversePulseClockWedges: { - effect: () => U.gsap.timeline({ duration: 0 }), - defaults: {} - }, - fillCoins: { - effect: (targets, config) => { - // Targets will be all coins from zero to where fill currently is - // Some will already be full, others not. - // Stagger in timeline - // Pulse in size and color - // Shimmer as they shrink back ? - return U.gsap.effects.throb(targets, { stagger: { - amount: 0.25, - from: "start", - repeat: 1, - yoyo: true - }, ...config ?? {} }); + blurReveal: { + effect: (target, config) => { + return U.gsap.effects.blurRemove(target, config).reverse(0); }, - defaults: {} + defaults: { + ignoreMargin: false, + skewX: -20, + duration: 0.5, + x: "+=300", + scale: 1.5, + filter: "blur(10px)" + }, + extendTimeline: true }, hoverTooltip: { effect: (tooltip, _config) => { @@ -827,7 +794,56 @@ export const gsapEffects = { defaults: { tooltipScale: 0.75 } + }, + textJitter: { + effect: (target, config) => { + const [targetElem] = $(target); + if (!targetElem) { + throw new Error("textJitter effect: target not found"); + } + const split = new SplitText(targetElem, { type: "chars" }); + return U.gsap.timeline() + .to(targetElem, { + autoAlpha: 1, + duration: config.duration, + ease: "none" + }) + .fromTo(split.chars, { + y: -config.yAmp + }, { + y: config.yAmp, + duration: config.duration, + ease: "sine.inOut", + stagger: { + repeat: -1, + yoyo: true, + from: "random", + each: config.stagger + } + }, 0) + .fromTo(split.chars, { + rotateZ: -config.rotateAmp + }, { + rotateZ: config.rotateAmp, + duration: config.duration, + ease: CustomWiggle.create("myWiggle", { wiggles: 10, type: "random" }), + stagger: { + repeat: -1, + from: "random", + yoyo: true, + each: config.stagger + } + }, 0); + }, + defaults: { + yAmp: 2, + rotateAmp: 1, + duration: 1, + stagger: 0.05 + }, + extendTimeline: true } + // #endregion }; /** * Registers relevant GSAP plugins and effects. diff --git a/module/core/helpers.js b/module/core/helpers.js index 42402081..7c786d2e 100644 --- a/module/core/helpers.js +++ b/module/core/helpers.js @@ -19,7 +19,6 @@ export async function preloadHandlebarsTemplates() { "systems/eunos-blades/templates/components/portrait.hbs", "systems/eunos-blades/templates/components/clock.hbs", "systems/eunos-blades/templates/components/roll-collab-mod.hbs", - "systems/eunos-blades/templates/components/roll-collab-opposition.hbs", "systems/eunos-blades/templates/components/slide-out-controls.hbs", "systems/eunos-blades/templates/components/consequence.hbs", "systems/eunos-blades/templates/components/consequence-accepted.hbs", diff --git a/module/core/utilities.js b/module/core/utilities.js index 8982da17..fc6f80a4 100644 --- a/module/core/utilities.js +++ b/module/core/utilities.js @@ -1063,7 +1063,25 @@ function objMap(obj, keyFunc, valFunc) { return [keyFuncTyped(key, val), valFuncTyped(val, key)]; })); } -const objSize = (obj) => Object.values(obj).filter((val) => val !== undefined && val !== null).length; +/** + * This function returns the 'size' of any reference passed into it, following these rules: + * - object: the number of enumerable keys + * - array: the number of elements + * - false/null/undefined: 0 + * - anything else: 1 + */ +const objSize = (obj) => { + if (isSimpleObj(obj)) { + return Object.keys(obj).length; + } + if (isArray(obj)) { + return obj.length; + } + if (obj === false || obj === null || obj === undefined) { + return 0; + } + return 1; +}; /** * This function is an object-equivalent of Array.findIndex() function. * It accepts check functions for both keys and/or values. @@ -1429,37 +1447,6 @@ const getSvgPaths = (svgDotKey, svgPathKeys) => { } return returnData; }; -// #region ░░░░░░░[GreenSock]░░░░ Wrappers for GreenSock Functions ░░░░░░░ ~ -const set = (targets, vars) => gsap.set(targets, vars); -/** - * - * @param target - * @param property - * @param unit - */ -function get(target, property, unit) { - if (unit) { - const propVal = regExtract(gsap.getProperty(target, property, unit), /[\d.]+/); - if (typeof propVal === "string") { - return pFloat(propVal); - } - throw new Error(`Unable to extract property '${property}' in '${unit}' units from ${target}`); - } - return gsap.getProperty(target, property); -} -const getGSAngleDelta = (startAngle, endAngle) => signNum(roundNum(getAngleDelta(startAngle, endAngle), 2)).replace(/^(.)/, "$1="); -// const Animate = { -// Timeline: { -// to: (tl: gsap.core.Timeline, targets: gsap.TweenTarget[], vars: gsap.TweenVars, position: any) => { -// if (targets.length === 0) { -// } -// } -// } (tl: gsap.core.Timeline, ) -// } -// const to = (targets: gsap.TweenTarget[], vars: gsap.TweenVars): gsap.core.Tween => { -// gsap. -// } -// #endregion ░░░░[GreenSock]░░░░ // #region ░░░░░░░[SVG]░░░░ SVG Generation & Manipulation ░░░░░░░ ~ const getRawCirclePath = (r, { x: xO, y: yO } = { x: 0, y: 0 }) => { [r, xO, yO] = [r, xO, yO].map((val) => roundNum(val, 2)); @@ -1572,6 +1559,66 @@ const escapeHTML = (str) => (typeof str === "string" .replace(/"/g, """) .replace(/[`']/g, "'") : str); +// #region ░░░░░░░[GreenSock]░░░░ Wrappers for GreenSock Functions ░░░░░░░ ~ +const set = (targets, vars) => gsap.set(targets, vars); +/** + * + * @param target + * @param property + * @param unit + */ +function get(target, property, unit) { + if (unit) { + const propVal = regExtract(gsap.getProperty(target, property, unit), /[\d.]+/); + if (typeof propVal === "string") { + return pFloat(propVal); + } + throw new Error(`Unable to extract property '${property}' in '${unit}' units from ${target}`); + } + return gsap.getProperty(target, property); +} +const getGSAngleDelta = (startAngle, endAngle) => signNum(roundNum(getAngleDelta(startAngle, endAngle), 2)).replace(/^(.)/, "$1="); +const getNearestLabel = (tl, matchTest) => { + if (!tl) { + return undefined; + } + if (!objSize(tl.labels)) { + return undefined; + } + if (typeof matchTest === "string") { + matchTest = new RegExp(matchTest); + } + // Filter the labels against the matchTest, if one provided, and sort by time in ascending order. + const labelTimes = Object.entries(tl.labels) + .filter(([label]) => { + return matchTest instanceof RegExp + ? matchTest.test(label) + : true; + }) + .sort((a, b) => a[1] - b[1]); + // Snap the current time of the timeline to the values in labelTimes + const nearestTime = gsap.utils.snap(labelTimes.map(([_label, time]) => time), tl.time()); + // Get the associated label for the nearest time + const [nearestLabel] = labelTimes.find(([_label, time]) => time === nearestTime); + return nearestLabel; +}; +const reverseRepeatingTimeline = (tl) => { + // FIRST: Determine if timeline itself is repeating, or if most-recent child tween of timeline is repeating + if (tl.repeat() === -1) { + // Timeline itself is repeating. Set totalTime equal to time, reverse. + tl.totalTime(tl.time()); + } + else { + // Get currently-running child tween, check if that is repeating. + const [tw] = tl.getChildren(false, true, true, tl.time()); + if (tw && tw.repeat() === -1) { + // Child tween is repeating. Set totalTime of TWEEN equal to time, reverse TIMELINE. + tw.totalTime(tw.time()); + } + tl.reverse(); + } +}; +// #endregion ░░░░[GreenSock]░░░░ // #endregion ▄▄▄▄▄ HTML ▄▄▄▄▄ // #region ████████ ASYNC: Async Functions, Asynchronous Flow Control ████████ ~ const sleep = (duration) => new Promise((resolve) => { @@ -1782,13 +1829,13 @@ export default { // ████████ HTML: Parsing HTML Code, Manipulating DOM Objects ████████ getSvgCode, getSvgPaths, changeContainer, - // ░░░░░░░ GreenSock ░░░░░░░ - gsap, get, set, getGSAngleDelta, - TextPlugin, Flip, MotionPathPlugin, getRawCirclePath, drawCirclePath, getColorVals, getRGBString, getHEXString, getContrastingColor, getRandomColor, getSiblings, escapeHTML, + // ░░░░░░░ GreenSock ░░░░░░░ + gsap, get, set, getGSAngleDelta, getNearestLabel, reverseRepeatingTimeline, + TextPlugin, Flip, MotionPathPlugin, // ████████ ASYNC: Async Functions, Asynchronous Flow Control ████████ sleep, // EVENT HANDLERS diff --git a/module/sheets/item/BladesClockKeeperSheet.js b/module/sheets/item/BladesClockKeeperSheet.js index 1efea0ec..cad9320b 100644 --- a/module/sheets/item/BladesClockKeeperSheet.js +++ b/module/sheets/item/BladesClockKeeperSheet.js @@ -42,7 +42,8 @@ class BladesClockKeeperSheet extends BladesItemSheet { async activateListeners(html) { super.activateListeners(html); function getClockKeyFromEvent(event) { - const id = $(event.currentTarget).data("keyId"); + const id = $(event.currentTarget).data("keyId") + || $(event.currentTarget).closest(".clock-key-control-flipper").data("clockKeyId"); if (!id) { throw new Error("No id found on element"); } @@ -84,6 +85,26 @@ class BladesClockKeeperSheet extends BladesItemSheet { await getClockKeyFromEvent(event).pull_SocketCall(); } }); + html.find("[data-action=\"toggle-name-visibility\"]").on({ + click: async (event) => { + event.preventDefault(); + const clockKey = getClockKeyFromEvent(event); + clockKey.updateTarget("isNameVisible", !clockKey.isNameVisible); + // If clockKey is in this scene and isVisible, must send out socket calls for animating name fading in/out + if (clockKey.isInCurrentScene && clockKey.isVisible) { + if (clockKey.isNameVisible) { + clockKey.fadeOutName_SocketCall(); + } + else { + clockKey.fadeInName_SocketCall(); + } + } + } + }); + html.find("input.clock-key-input:not([readonly])").on({ change: async (event) => { + const input$ = $(event.currentTarget); + await getClockKeyFromEvent(event).updateTarget(input$.data("targetProp"), input$.val()); + } }); } } export default BladesClockKeeperSheet; diff --git a/module/sheets/item/BladesItemSheet.js b/module/sheets/item/BladesItemSheet.js index 6e25adaa..fa979f61 100644 --- a/module/sheets/item/BladesItemSheet.js +++ b/module/sheets/item/BladesItemSheet.js @@ -254,9 +254,6 @@ class BladesItemSheet extends ItemSheet { if (!(this.item instanceof BladesProject)) { return undefined; } - // if (this.item.clockKey) { - // this.item.clockKey.isShowingControls = game.user.isGM; - // } const sheetData = {}; return { ...context, diff --git a/scss/components/_clocks.scss b/scss/components/_clocks.scss index 7ea0a90c..8799a84f 100644 --- a/scss/components/_clocks.scss +++ b/scss/components/_clocks.scss @@ -293,8 +293,8 @@ // background: rgba(255, 255, 0, 0.25); // outline: 2px dotted rgb(255, 255, 0); - &:hover { - color: var(--blades-gold-bright) !important; + &.label-hidden { + visibility: hidden; } } @@ -344,7 +344,6 @@ .clock { position: relative; overflow: visible; - pointer-events: all !important; &, & * { diff --git a/scss/overlay/_blades-overlay.scss b/scss/overlay/_blades-overlay.scss index e040ac17..7bdf1828 100644 --- a/scss/overlay/_blades-overlay.scss +++ b/scss/overlay/_blades-overlay.scss @@ -21,6 +21,13 @@ $key-sizes: ( transform-style: preserve-3d; } + #test-box { + height: 200px; + width: 200px; + background: blue; + top: 200px; + } + @import "../components/clocks"; diff --git a/scss/sheets/_clock-keeper-sheet.scss b/scss/sheets/_clock-keeper-sheet.scss index b1e6961a..625e0284 100644 --- a/scss/sheets/_clock-keeper-sheet.scss +++ b/scss/sheets/_clock-keeper-sheet.scss @@ -112,17 +112,15 @@ display: flex; align-items: center; justify-content: stretch; - background: linear-gradient(45deg, black, rgb(40, 40, 40)); - color: white; - font-family: "Sura"; + background: linear-gradient(45deg, var(--blades-grey-bright), var(--blades-grey-dark)); font-size: 24px; - box-shadow: inset 0 0 2px 2px black; + box-shadow: inset 0 0 2px 2px var(--blades-black-dark); > div { position: absolute; visibility: hidden; white-space: nowrap; - box-shadow: inset 0 0 2px 2px black; + box-shadow: inset 0 0 2px 2px var(--blades-black-dark); display: flex; flex-direction: row; justify-content: space-between; @@ -135,8 +133,8 @@ border-radius: 5px; &.controls-back { - color: black; - background: white; + color: var(--blades-black); + background: linear-gradient(45deg, var(--blades-white-bright), var(--blades-grey-bright));; } } } diff --git a/templates/parts/clock-sheet-key-controls.hbs b/templates/parts/clock-sheet-key-controls.hbs index 0fceed05..51a1b1e6 100644 --- a/templates/parts/clock-sheet-key-controls.hbs +++ b/templates/parts/clock-sheet-key-controls.hbs @@ -32,8 +32,7 @@ {{!-- Name --}} @@ -65,6 +64,8 @@ action="add-key-to-scene" targetId=clockKey.id }} + {{/if}} + {{!-- Toggle Name Visibility --}} {{> "systems/eunos-blades/templates/components/button-icon.hbs" blockClass="clock-control-button clock-toggle-name-visible" @@ -72,9 +73,6 @@ action="toggle-name-visibility" targetId=clockKey.id }} - {{/if}} - - {{!-- Add Clock --}} {{> "systems/eunos-blades/templates/components/button-icon.hbs" @@ -86,8 +84,7 @@ {{!-- Name --}} diff --git a/templates/roll/partials/roll-collab-action-gm.hbs b/templates/roll/partials/roll-collab-action-gm.hbs index c2e4cac0..cea1a790 100644 --- a/templates/roll/partials/roll-collab-action-gm.hbs +++ b/templates/roll/partials/roll-collab-action-gm.hbs @@ -234,7 +234,20 @@

Consequences

diff --git a/templates/roll/partials/roll-collab-action.hbs b/templates/roll/partials/roll-collab-action.hbs index cf9650f8..e4fe256b 100644 --- a/templates/roll/partials/roll-collab-action.hbs +++ b/templates/roll/partials/roll-collab-action.hbs @@ -264,9 +264,20 @@
diff --git a/templates/roll/partials/roll-collab-fortune.hbs b/templates/roll/partials/roll-collab-fortune.hbs index c5718f0a..123c6dda 100644 --- a/templates/roll/partials/roll-collab-fortune.hbs +++ b/templates/roll/partials/roll-collab-fortune.hbs @@ -247,21 +247,33 @@ {{/if}} +
+ {{#if rollClockKey}} + {{> "systems/eunos-blades/templates/components/clock-key.hbs" rollClockKey}} + {{/if}} +
{{#if rollOpposition}} - {{#if rollOpposition.rollOppClock}} - {{> "systems/eunos-blades/templates/components/clock.hbs" rollOpposition.rollOppClock isShowingControls=false}} - {{else}} - {{/if}} {{/if}}
diff --git a/templates/roll/partials/roll-collab-indulgevice-gm.hbs b/templates/roll/partials/roll-collab-indulgevice-gm.hbs index d6246fef..8a33d576 100644 --- a/templates/roll/partials/roll-collab-indulgevice-gm.hbs +++ b/templates/roll/partials/roll-collab-indulgevice-gm.hbs @@ -1,265 +1,202 @@ -
-
-
-

{{rollType}} Roll: {{rollPrimary.rollPrimaryName}}

-
-
-
-
-
- {{#each rollFactorData}} -
- - {{value}} -
- {{/each}} - -
-
- + {{/each}} +
+ {{/if}} +
" 0)}}positive{{/if}}"> + {{signNum rollResultFinal}} Result Level +
- + {{/if}} + + \ No newline at end of file diff --git a/templates/roll/partials/roll-collab-indulgevice.hbs b/templates/roll/partials/roll-collab-indulgevice.hbs index d6246fef..123c6dda 100644 --- a/templates/roll/partials/roll-collab-indulgevice.hbs +++ b/templates/roll/partials/roll-collab-indulgevice.hbs @@ -1,22 +1,77 @@ +
+
{{rollType}} Roll
+
+ +
+
+ {{#each rollFactors.source}} + {{#if (test isActive "&&" isPrimary)}} +
+ + {{display}} +
+ {{/if}} + {{/each}} + {{#each rollFactors.opposition}} + {{#if (test isActive "&&" isPrimary)}} +
+ + {{display}} +
+ {{/if}} + {{/each}} +
+
+ +
+ {{rollPrimary.rollPrimaryName}} + {{#if rollOpposition}}vs.{{else}} {{/if}} + {{#if rollOpposition}}{{rollOpposition.rollOppName}}{{else}} {{/if}} +
+ +
+
+ {{#each rollFactors.source}} + {{#if isActive}}{{#unless isPrimary}} +
+ + {{display}} +
+ {{/unless}}{{/if}} + {{/each}} +
+
+ +{{#if (test gamePhase "==" "Downtime")}}{{#if (test rollPrimary.rollPrimaryType "==" "pc")}} +
+ {{gamePhase}} +
+{{/if}}{{/if}} +
+
+ +
+
-
-

{{rollType}} Roll: {{rollPrimary.rollPrimaryName}}

-
-
+
+ {{#if rollClockKey}} + {{> "systems/eunos-blades/templates/components/clock-key.hbs" rollClockKey}} + {{/if}} +
+
+ {{#if rollOpposition}} + + {{/if}} +
+
-
-
- {{#each rollFactorData}} -
+ -
+ \ No newline at end of file diff --git a/ts/classes/BladesClocks.ts b/ts/classes/BladesClocks.ts index fb62bf43..0c2c1268 100644 --- a/ts/classes/BladesClocks.ts +++ b/ts/classes/BladesClocks.ts @@ -15,7 +15,13 @@ class BladesClockKey extends BladesTargetLink implements function registerClockKeys(doc: BladesDoc) { if ("clocksData" in doc.system) { (Object.values(doc.system.clocksData ?? {}) as BladesClockKey.Data[]) - .forEach((keyData) => {new BladesClockKey(keyData);}); + .forEach((keyData) => { + try { + new BladesClockKey(keyData); + } catch(err) { + eLog.error("BladesClockKey", "[BladesClockKey.Initialize] Error initializing clock key.", err, keyData); + } + }); } } @@ -45,6 +51,8 @@ class BladesClockKey extends BladesTargetLink implements socketlib.system.register("pull_SocketCall", BladesClockKey.pull_SocketResponse.bind(this)); socketlib.system.register("drop_SocketCall", BladesClockKey.drop_SocketResponse.bind(this)); + socketlib.system.register("fadeInName_SocketCall", BladesClockKey.fadeInName_SocketResponse.bind(this)); + socketlib.system.register("fadeOutName_SocketCall", BladesClockKey.fadeOutName_SocketResponse.bind(this)); return loadTemplates([ "systems/eunos-blades/templates/components/clock-key.hbs", @@ -231,15 +239,6 @@ class BladesClockKey extends BladesTargetLink implements ); } - async appendToOverlay(): Promise> { - return game.eunoblades.Director.appendToClockKeySection(await this.getHTML()); - } - async removeFromOverlay(): Promise { - delete this._hoverOverTimeline; - delete this._keySwingTimeline; - return game.eunoblades.Director.removeFromClockKeySection(this.id); - } - get elem(): HTMLElement | undefined { return $(`#${this.id}`)[0]; } @@ -252,11 +251,11 @@ class BladesClockKey extends BladesTargetLink implements get containerElem$(): JQuery | undefined { return this.containerElem ? $(this.containerElem) : undefined; } - - get isShowingControls(): boolean { - if (!this.elem) {return false;} - if (!game.user.isGM) {return false;} - return !$(this.elem).hasClass("controls-hidden"); + get labelElem(): HTMLInputElement | undefined { + return this.elem$ ? this.elem$.find(".key-label")[0] as HTMLInputElement : undefined; + } + get labelElem$(): JQuery | undefined { + return this.elem$ ? this.elem$.find(".key-label") as JQuery: undefined; } // Initializes clock key with proper position and scale before displaying via autoAlpha @@ -461,28 +460,55 @@ class BladesClockKey extends BladesTargetLink implements // #region > TIMELINES ~ _keySwingTimeline?: gsap.core.Timeline; - get keySwingTimeline() { - if (!this.elem) {return undefined;} - if (!$(this.elem).parents("#blades-overlay").length) {return undefined;} + get keySwingTimeline(): gsap.core.Timeline { + if (!this.elem) {throw new Error("elem is not defined for keySwingTimeline");} + if (!$(this.elem).parents("#blades-overlay").length) {throw new Error("elem is not a child of #blades-overlay");} if (!this._keySwingTimeline) { this._keySwingTimeline = U.gsap.effects.keySwing(this.elem).pause(); } - return this._keySwingTimeline; + return this._keySwingTimeline as gsap.core.Timeline; } _hoverOverTimeline?: gsap.core.Timeline; - get hoverOverTimeline() { - if (!this.elem) {return undefined;} + get hoverOverTimeline(): gsap.core.Timeline { + if (!this.elem) {throw new Error("elem is not defined for hoverOverTimeline");} + if (!$(this.elem).parents("#blades-overlay").length) {throw new Error("elem is not a child of #blades-overlay");} if (!this._hoverOverTimeline) { - this._hoverOverTimeline = U.gsap.effects.hoverOverClockKey(this); + this._hoverOverTimeline = U.gsap.effects.hoverOverClockKey(this.elem); } - return this._hoverOverTimeline; + return this._hoverOverTimeline as gsap.core.Timeline; + } + + _nameFadeInTimeline?: gsap.core.Timeline; + get nameFadeInTimeline(): gsap.core.Timeline { + if (!this.labelElem$) {throw new Error("labelElem$ is not defined for nameFadeInTimeline");} + if (!this.elem$?.parents("#blades-overlay")?.length) {throw new Error("elem is not a child of #blades-overlay");} + if (!this._nameFadeInTimeline) { + U.gsap.killTweensOf(this.labelElem$); + this._nameFadeInTimeline = U.gsap.effects.blurReveal(this.labelElem$, { + ignoreMargin: true, + duration: 1.5, + callbackScope: this, + onStart() { + this.labelElem$.removeClass("label-hidden"); + }, + onComplete() { + U.gsap.effects.textJitter(this.labelElem$); + }, + onReverseComplete() { + this.labelElem$.addClass("label-hidden"); + U.gsap.killTweensOf(this.labelElem$); + delete this._nameFadeInTimeline; + } + }).pause(); + } + return this._nameFadeInTimeline as gsap.core.Timeline; } // #endregion // #region > SOCKET CALLS: _SocketCall / static _SocketResponse / _Animation async drop_Animation(callback?: () => void) { - await this.appendToOverlay(); + await game.eunoblades.Director.appendClockKeyToOverlay(this); U.gsap.effects.keyDrop(this.elem, {callback}); this.keySwingTimeline?.seek(0).play(); } @@ -502,7 +528,7 @@ class BladesClockKey extends BladesTargetLink implements await new Promise((resolve) => { U.gsap.effects.keyPull(this.elem, {callback}).then(resolve); }); - this.removeFromOverlay(); + game.eunoblades.Director.removeClockKeyFromOverlay(this); } async pull_SocketCall() { if (!game.user.isGM) {return;} @@ -516,6 +542,48 @@ class BladesClockKey extends BladesTargetLink implements if (!key) {return;} key.pull_Animation(); } + + async fadeInName_Animation(callback?: () => void) { + if (!this.labelElem$) { return; } + if (!this.name) { return; } + this.nameFadeInTimeline.play(); + if (callback) { + U.gsap.delayedCall(2, callback); + } + } + async fadeInName_SocketCall() { + if (!game.user.isGM) {return;} + if (!this.elem) {return;} + if (!$(this.elem).parents("#blades-overlay").length) {return;} + this.fadeInName_Animation(); + socketlib.system.executeForOthers("fadeInName_SocketCall", this.id); + } + static fadeInName_SocketResponse(keyID: IDString) { + const key = game.eunoblades.ClockKeys.get(keyID); + if (!key) {return;} + key.fadeInName_Animation(); + } + + async fadeOutName_Animation(callback?: () => void) { + if (!this.labelElem$) { return; } + if (!this.name) { return; } + this.nameFadeInTimeline.reverse(); + if (callback) { + U.gsap.delayedCall(2, callback); + } + } + async fadeOutName_SocketCall() { + if (!game.user.isGM) {return;} + if (!this.elem) {return;} + if (!$(this.elem).parents("#blades-overlay").length) {return;} + this.fadeOutName_Animation(); + socketlib.system.executeForOthers("fadeOutName_SocketCall", this.id); + } + static fadeOutName_SocketResponse(keyID: IDString) { + const key = game.eunoblades.ClockKeys.get(keyID); + if (!key) {return;} + key.fadeOutName_Animation(); + } // #endregion // #endregion @@ -610,7 +678,6 @@ class BladesClock extends BladesTargetLink implements Blades if (!pKey) { throw new Error(`[BladesClockKey.parentKey] No parent key found for clock ${this.id}`);} return pKey; } - get isShowingControls(): boolean { return this.parentKey.isShowingControls; } get isNameVisible(): boolean {return U.pBool(this.data.isNameVisible);} set isNameVisible(val: boolean) {this.updateTarget("isNameVisible", U.pBool(val));} @@ -668,7 +735,7 @@ class BladesClock extends BladesTargetLink implements Blades // #region HTML INTERACTION ~ get elem(): HTMLElement | undefined { - return $(`#${this.id}"`)[0]; + return $(`#${this.id}`)[0]; } get elem$(): JQuery | undefined { return this.elem ? $(this.elem) : undefined; diff --git a/ts/classes/BladesDirector.ts b/ts/classes/BladesDirector.ts index c3bdc0bc..34fe1f65 100644 --- a/ts/classes/BladesDirector.ts +++ b/ts/classes/BladesDirector.ts @@ -60,7 +60,6 @@ class BladesDirector { if (!this._overlayContainer) { $("body.vtt").append("
"); [this._overlayContainer] = $("#blades-overlay"); - this.resetObservers(); } return this._overlayContainer; } @@ -75,21 +74,19 @@ class BladesDirector { private get clockKeySection$(): JQuery { return this.overlayContainer$.find(".overlay-section-clock-keys"); } - public async appendToClockKeySection(elem: HTMLCode|HTMLElement|JQuery): Promise> { - if (typeof elem === "string") { - elem = $(elem); - } - elem = $(elem).appendTo(this.clockKeySection$); - const keyID = elem.find(".clock-key").data("id") as IDString; - const key = game.eunoblades.ClockKeys.get(keyID) as BladesClockKey; - await key.initClockKeyElem(); - return key.elem$ as JQuery; - } - public removeFromClockKeySection(elem: IDString|HTMLElement|JQuery): void { - if (typeof elem === "string") { - elem = $(`#${elem}`); - } - $(elem).closest(".clock-key-container").remove(); + + public async appendClockKeyToOverlay(clockKey: BladesClockKey): Promise> { + const clockKeyHTML = await clockKey.getHTML(); + $(clockKeyHTML).appendTo(this.clockKeySection$); + if (!clockKey.containerElem$) { throw new Error("ClockKey container element not found."); } + this.activateClockListeners(clockKey.containerElem$); + return clockKey.containerElem$; + } + + public removeClockKeyFromOverlay(clockKey: BladesClockKey): void { + delete clockKey._hoverOverTimeline; + delete clockKey._keySwingTimeline; + clockKey.containerElem$?.remove(); } private get locationSection$(): JQuery { @@ -123,68 +120,6 @@ class BladesDirector { private get svgData() {return SVGDATA;} // #endregion - - // #region OBSERVERS ~ - - get ObserverData(): Observer.ObserverVars[] { - return [ - // Overlay Clock Key Observer - { - id: "overlay-clock-key", - type: "pointer", - target: game.eunoblades.Director.clockKeySection$[0], - ignore: [ - ...ObserverIgnoreStrings - ], - onPress(obs) { - if (!(obs.event.currentTarget instanceof HTMLElement)) { return; } - const target$ = $(obs.event.currentTarget); - if (target$.hasClass("clock-key")) { - const clockKey = game.eunoblades.ClockKeys.get(target$.attr("id") ?? ""); - if (!clockKey) { throw new Error(`ClockKey not found for ID: '${target$.attr("id") ?? ""}'`); } - switch (obs.event.type) { - case "dblclick": { - console.log(`Double-Click on ClockKey: ${clockKey.name || clockKey.id}`); - break; - } - case "contextmenu": { - console.log(`Right-Click on ClockKey: ${clockKey.name || clockKey.id}`); - break; - } - default: { - break; - } - } - } - } - } - ]; - } - - _Observers?: Collection; - - get Observers(): Collection { - return this._Observers ??= new Collection( - this.ObserverData - .map((oVars) => { - if (!oVars.id) { - eLog.error("BladesDirector", "Observer must have an ID", oVars); - throw new Error("Observer must have an ID"); - } - return [oVars.id, Observer.create(oVars)]; - }) - ); - } - - resetObservers() { - this._Observers?.forEach((obs) => {obs.kill();}); - this._Observers?.clear(); - delete this._Observers; - void this.Observers; // Trigger Observer regeneration within getter. - } - - // #endregion - get sceneKeys() {return game.eunoblades.ClockKeeper.getSceneKeys();} renderOverlay_SocketCall() { @@ -204,32 +139,54 @@ class BladesDirector { // Display keys that are visible this.sceneKeys .filter((key) => key.isVisible) - .forEach((key) => key.drop_Animation()); + .forEach((key) => key.drop_Animation((async () => { + await game.eunoblades.Director.activateClockListeners(key.containerElem$); + if (key.isNameVisible) { + key.nameFadeInTimeline.progress(0.99).play(); + } + }))); } - private async activateClockListeners() { + private async activateClockListeners(keyContainers$: JQuery = this.clockKeySection$.find(".clock-key-container")) { - this.clockKeySection$.find(".clock-key-container").each((_, keyContainer) => { + keyContainers$.each((_, keyContainer) => { const keyContainer$ = $(keyContainer); const clockKey = game.eunoblades.ClockKeys.get(keyContainer$.find(".clock-key").attr("id") ?? ""); if (!clockKey) {return;} - // Enable pointer events on the container, so that the hover-over timeline can be played - keyContainer$.css("pointer-events", "auto"); + // The ".key-bg" child is actually the correct shape, so that will be our listener object. + const keyListener$ = clockKey.elem$?.find(".key-bg"); + if (!keyListener$?.[0]) { return; } + + // Enable pointer events on the key-bg, so that the hover-over timeline can be played, + // and remove any existing listeners to avoid duplication + keyListener$.css("pointer-events", "auto"); + keyListener$.off(); + // Do the same for clocks contained by the key clockKey.clocks.forEach((clock) => { - if (!clock.elem) {return;} - $(clock.elem).css("pointer-events", "auto"); + if (!clock.elem$) {return;} + clock.elem$.css("pointer-events", "auto"); + clock.elem$.off(); }); if (game.user.isGM) { // === GM-ONLY LISTENERS === // Double-Click a Clock Key = SocketPull it, Open ClockKeeper sheet - keyContainer$.on("dblclick", async () => { + keyListener$.on("dblclick", async () => { clockKey.pull_SocketCall(); - game.eunoblades.ClockKeeper.render(true); + if (!game.eunoblades.ClockKeeper.sheet?.rendered) { + game.eunoblades.ClockKeeper.render(true); + } + }); + + // Right-Click a Clock Key = Open ClockKeeper sheet. + keyListener$.on("dblclick", async () => { + if (!game.eunoblades.ClockKeeper.sheet?.rendered) { + game.eunoblades.ClockKeeper.render(true); + } }); // Mouse-Wheel a Clock = Add/Remove Segments one-by-one. @@ -259,10 +216,10 @@ class BladesDirector { // === PLAYER-ONLY LISTENERS === // Add listeners to container for mouseenter and mouseleave, that play and reverse timeline attached to element - keyContainer$.on("mouseenter", () => { - clockKey.hoverOverTimeline?.play(); + keyListener$.on("mouseenter", () => { + clockKey.hoverOverTimeline.play(); }).on("mouseleave", () => { - clockKey.hoverOverTimeline?.reverse(); + U.reverseRepeatingTimeline(clockKey.hoverOverTimeline); }); // Now repeat this for each clock in the clock key @@ -274,7 +231,9 @@ class BladesDirector { clockElem$.on("mouseenter", () => { clock.hoverOverTimeline?.play(); }).on("mouseleave", () => { - clock.hoverOverTimeline?.reverse(); + if (clock.hoverOverTimeline) { + U.reverseRepeatingTimeline(clock.hoverOverTimeline); + } }); }); } diff --git a/ts/core/gsap.ts b/ts/core/gsap.ts index ad2a189e..85de38ff 100644 --- a/ts/core/gsap.ts +++ b/ts/core/gsap.ts @@ -20,43 +20,20 @@ const gsapPlugins: gsap.RegisterablePlugins[] = [ export type gsapConfig = gsap.TweenVars & { duration: number, - targets: Record|Array>> + targets: Record | Array>> } export type gsapEffect = { effect: ( targets: gsap.TweenTarget, config: gsap.TweenVars & {duration: number} - ) => gsap.core.Timeline|gsap.core.Tween, + ) => gsap.core.Timeline | gsap.core.Tween, defaults: gsap.TweenVars, extendTimeline?: boolean } export const gsapEffects: Record = { - hoverButton: { - effect: (target, config) => { - return U.gsap.timeline({paused: true}) - .to(target, { - scale: config.scale, - ease: "power2", - duration: config.duration - }) - .fromTo(target, { - filter: config.brightness ? "brightness(1)" : undefined - }, { - color: config.color, - filter: config.brightness ? `brightness(${config.brightness})` : undefined, - duration: config.duration, - ease: "sine" - }, 0); - }, - defaults: { - color: undefined, - brightness: 1.5, - duration: 0.5, - scale: 1.25 - } - }, + // #region CLOCK KEYS keyDrop: { effect: (clockKey, config) => { const [keyContainer] = $(clockKey as HTMLElement).closest(".clock-key-container"); @@ -87,14 +64,14 @@ export const gsapEffects: Record = { const [keyContainer] = $(clockKey as HTMLElement).closest(".clock-key-container"); // Get initial scale, - const tl = U.gsap.timeline({id: "keySwing", repeat: -1, yoyo: true, data: {labelTimes: {}}}) + const tl = U.gsap.timeline({id: "keySwing", repeat: -1, yoyo: true}) .fromTo(keyContainer, { transformOrigin: "50% 10%", rotateZ: -config.swingAngle }, { rotateZ: config.swingAngle, ease: "sine.inOut", - duration: config.duration / 4, + duration: 0.25 * config.duration, repeat: 2, yoyo: true }) @@ -105,16 +82,27 @@ export const gsapEffects: Record = { top: `-=${0.5 * config.yRange}`, scale: `-=${0.5 * config.scaleRange}`, ease: "sine.inOut", - duration: config.duration - }, 0); + duration: 0.75 * config.duration + }, 0.25 * config.duration); + + // Get times where rotateZ is 0 + const timesWhenRotateZIsZero = Array(4).fill(config.duration / 8) + .map((val, index) => val * (2 * (index + 1))); + + // Get time when top & scale shifts are 0 + const timeWhenShiftsAreZero = ((0.75 * config.duration) / 2) + (0.25 * config.duration); - // Add labels at the points where rotateZ is 0, and log these times to a snappable object literal in the data property - const timesWhenRotateZIsZero = Array(4).fill(config.duration / 8).map((val, index) => val * (2 * (index + 1))); + // Add labels to all rotateZ === 0 times, but if shifts are also zero, name it "NEUTRAL" timesWhenRotateZIsZero.forEach((timestamp, i) => { tl.addLabel(`rotateZZero${i}`, timestamp); - tl.data.labelTimes[timestamp] = `rotateZZero${i}`; + if (timestamp === timeWhenShiftsAreZero && tl.labels.NEUTRAL === undefined) { + tl.addLabel("NEUTRAL", timestamp); + } }); + // Immediately move the timeline to the "NEUTRAL" label, so the timeline begins from there + tl.seek("NEUTRAL"); + return tl; }, defaults: { @@ -168,87 +156,86 @@ export const gsapEffects: Record = { }, extendTimeline: true }, - keyControlZoom: { - effect: (clockKeyElem: gsap.TweenTarget, config: gsap.TweenVars & {duration: number}) => { - if (!clockKeyElem) { throw new Error("clockKeyElem is null or undefined"); } - const clockKey = game.eunoblades.ClockKeys.get($(clockKeyElem as HTMLElement).attr("id") ?? ""); - if (!clockKey) { throw new Error("clockKey is null or undefined"); } - if (!clockKey.containerElem) { throw new Error("clockKey.containerElem is null or undefined"); } - - const tl = U.gsap.timeline({ - id: "keyZoom", - paused: true, - onStart() { - // Get the keySwing timeline, if there is one - const keySwingTimeline = clockKey.keySwingTimeline as gsap.core.Timeline - & {data: {labelTimes: Record}}; - - if (keySwingTimeline) { - // Get the current time and duration of the timeline - const currentTime = keySwingTimeline.time(); - const duration = keySwingTimeline.duration(); - - // Snap to the nearest label time - const nearestLabelTime = U.gsap.utils.snap( - Object.keys(keySwingTimeline.data.labelTimes).map(U.pInt), - currentTime - ); - - // Get associated label - const nearestLabel = keySwingTimeline.data.labelTimes[nearestLabelTime]; - - // Animate to the nearest label, then seek to the midpoint of the animation, where scale and vertical offsets are also zero. - keySwingTimeline.tweenTo(nearestLabel, {duration: 0.25, ease: "none"}).then(() => keySwingTimeline.seek(duration / 2).pause()); - } - }, - onReverseComplete() { - clockKey.keySwingTimeline?.resume(); - } - }).to(clockKey.containerElem, { - scale: config.scale, - ease: config.ease, - duration: config.duration, - onStart() { - clockKey.containerElem$?.removeClass("controls-hidden"); - }, - onReverseComplete() { - clockKey.containerElem$?.addClass("controls-hidden"); - } - }); - return tl; + keyNameFadeIn: { + effect: (target, config) => { + return U.gsap.effects.blurReveal(target, config); }, defaults: { + ignoreMargin: true, + skewX: -20, + duration: 0.5, + x: "+=300", scale: 1.5, - ease: "sine", - duration: 1 - } + filter: "blur(10px)" + }, + extendTimeline: true }, hoverOverClockKey: { - effect: (clockKey, config) => { - if (!(clockKey instanceof BladesClockKey)) { throw new Error("clockKey is not an instance of BladesClockKey"); } - if (!clockKey.elem) { throw new Error("clockKey.elem is null or undefined"); } + effect: (clockKeyElem, config) => { + if (!clockKeyElem) {throw new Error("clockKeyElem is null or undefined");} + const clockKey = game.eunoblades.ClockKeys.get($(clockKeyElem as HTMLElement).attr("id") ?? ""); + if (!clockKey) {throw new Error("clockKey is null or undefined");} + if (!clockKey.elem$) {throw new Error("clockKey.elem$ is null or undefined");} + if (!clockKey.containerElem$) {throw new Error("clockKey.containerElem$ is null or undefined");} + const clockKeyHiddenLabel$ = clockKey.elem$.find(".key-label.hidden-label"); + const clockKeyLabel$ = clockKey.elem$.find(".key-label"); + + // Construct master timeline, + const tl = U.gsap.timeline({paused: true}); + + // Create initial tween that resets keySwing to neutral + tl.add(clockKey.keySwingTimeline + .tweenTo("NEUTRAL", { + duration: 0.25 * config.duration, + ease: "none" + }) + ); - return U.gsap.timeline({paused: true}) - .to(clockKey.elem, { - scale: 1.25, - ease: "sine", - duration: 0.25, - onStart() { - clockKey.keySwingTimeline?.tweenTo(1, {duration: 0.25, ease: "none"}); - }, - onReverse() { - clockKey.keySwingTimeline?.resume(); - } - }, 0); + // Add a label for the proper start of the hover-over animation + tl.addLabel("hoverStart"); + + // Add an initial callback that resumes keySwing if the timeline hits this point while reversed + tl.add(() => { + if (tl.reversed()) { + // Immediately seek to the beginning, so keySwing is reset on another hover-over + tl.seek(0).pause(); + clockKey.keySwingTimeline.seek("NEUTRAL").play(); + } + }); + + // === HOVER-OVER ANIMATION === + // Brighten & enlarge clockKey + tl.fromTo(clockKeyElem, { + filter: "brightness(1)" + }, { + filter: `brightness(${config.brightness})`, + scale: function(i, target) { + return (U.gsap.getProperty(target, "scale") as number) * config.scaleMult; + }, + duration: 0.75 * config.duration + }, "hoverStart"); + + // Fade in name + tl.blurReveal(clockKeyHiddenLabel$, { + ignoreMargin: true, + duration: 0.75 * config.duration + }, "hoverStart"); + + // Move into repeating jitter tween + tl.textJitter(clockKeyLabel$); + + return tl; }, defaults: { - + duration: 1.5, + brightness: 1.5, + scaleMult: 1.25 } }, hoverOverClock: { effect: (clock, config) => { - if (!(clock instanceof BladesClock)) { throw new Error("clock is not an instance of BladesClock"); } - if (!clock.elem) { throw new Error("clock.elem is null or undefined"); } + if (!(clock instanceof BladesClock)) {throw new Error("clock is not an instance of BladesClock");} + if (!clock.elem) {throw new Error("clock.elem is null or undefined");} const [clockLabel] = $(clock.elem).find(".clock-label"); const [clockGlow] = $(clock.elem).find(".clock-glow"); @@ -272,6 +259,9 @@ export const gsapEffects: Record = { duration: 0.5 } }, + // #endregion + + // #region CHAT CONSEQUENCE EFFECTS csqEnter: { effect: (csqContainer, config) => { const csqRoot = U.gsap.utils.selector(csqContainer); @@ -284,7 +274,7 @@ export const gsapEffects: Record = { const csqAcceptNameElem = csqRoot(".consequence-name.accept-consequence"); // const csqAcceptElems = csqRoot(".accept-consequence:not(.consequence-icon-circle):not(.consequence-button-container)"); - const tl = U.gsap.timeline({paused: true, defaults: { }}); + const tl = U.gsap.timeline({paused: true, defaults: {}}); // Initialize name and type opacities. if (csqAcceptTypeElem.length > 0) { @@ -475,7 +465,7 @@ export const gsapEffects: Record = { const buttonIcon = buttonRoot(".button-icon i"); const buttonLabel = buttonRoot(".consequence-button-label"); - const tl = U.gsap.timeline({paused: true, defaults: { }}); + const tl = U.gsap.timeline({paused: true, defaults: {}}); // Turn type line white if (typeLine.length > 0) { @@ -567,7 +557,7 @@ export const gsapEffects: Record = { const acceptIconCircle = csqRoot(".consequence-icon-circle.accept-consequence"); const acceptButton = csqRoot(".consequence-button-container.consequence-accept-button-container"); - const tl = U.gsap.timeline({paused: true, defaults: { }}); + const tl = U.gsap.timeline({paused: true, defaults: {}}); // Fade out type line if (typeLine.length > 0) { @@ -626,7 +616,7 @@ export const gsapEffects: Record = { const footerBg = csqRoot(`.consequence-footer-container .consequence-footer-bg.${config.type}-consequence`); const footerMsg = csqRoot(`.consequence-footer-container .consequence-footer-message.${config.type}-consequence`); - const tl = U.gsap.timeline({paused: true, defaults: { }}); + const tl = U.gsap.timeline({paused: true, defaults: {}}); // Fade in icon circle if (iconCircle.length > 0) { @@ -747,6 +737,42 @@ export const gsapEffects: Record = { }, defaults: {} }, + // #endregion + + // #region CHARACTER SHEET EFFECTS + fillCoins: { + effect: (targets, config) => { + // Targets will be all coins from zero to where fill currently is + // Some will already be full, others not. + // Stagger in timeline + // Pulse in size and color + // Shimmer as they shrink back ? + return U.gsap.to(targets, + { + duration: config.duration / 2, + scale: config.scale, + filter: config.filter, + ease: config.ease, + stagger: { + amount: 0.25, + from: "start", + repeat: 1, + yoyo: true + } + } + ); + }, + defaults: { + duration: 1, + scale: 1, + filter: "saturate(1) brightness(2)", + ease: "power2.in" + }, + extendTimeline: true + }, + // #endregion + + // #region GENERAL: 'blurRemove', 'hoverTooltip', 'textJitter' blurRemove: { effect: (targets, config) => U.gsap.timeline() .to( @@ -761,12 +787,16 @@ export const gsapEffects: Record = { targets, { x: config.x, - marginBottom(i, target) { - return U.get(target, "height") as number * -1; - }, - marginRight(i, target) { - return U.get(target, "width") as number * -1; - }, + marginBottom: config.ignoreMargin + ? undefined + : function(i, target) { + return U.get(target, "height") as number * -1; + }, + marginRight: config.ignoreMargin + ? undefined + : function(i, target) { + return U.get(target, "width") as number * -1; + }, scale: config.scale, filter: config.filter, duration: (3 / 4) * config.duration @@ -783,94 +813,28 @@ export const gsapEffects: Record = { config.duration / 2 ), defaults: { + ignoreMargin: false, skewX: -20, duration: 0.5, x: "+=300", scale: 1.5, filter: "blur(10px)" - } - }, - slideUp: { - effect: (targets) => U.gsap.to( - targets, - { - height: 0, - // PaddingTop: 0, - // paddingBottom: 0, - duration: 0.5, - ease: "power3" - } - ), - defaults: {} - }, - pulse: { - effect: (targets, config) => U.gsap.to( - targets, - { - repeat: config.repCount, - yoyo: true, - duration: config.duration / config.repCount, - ease: config.ease, - opacity: 0.25 - } - ), - defaults: { - repCount: 3, - duration: 5, - ease: "sine.inOut" - } - }, - throb: { - effect: (targets, config) => U.gsap.to( - targets, - { - repeat: config.stagger ? undefined : 1, - yoyo: config.stagger ? undefined : true, - duration: config.duration / 2, - scale: config.scale, - filter: config.filter, - ease: config.ease, - stagger: config.stagger - ? { - ...config.stagger as gsap.StaggerVars, - repeat: 1, - yoyo: true - } - : {} - } - ), - defaults: { - duration: 1, - scale: 1, - filter: "saturate(1) brightness(2)", - ease: "power2.in" }, extendTimeline: true }, - pulseClockWedges: { - effect: () => U.gsap.timeline({duration: 0}), - defaults: {} - }, - reversePulseClockWedges: { - effect: () => U.gsap.timeline({duration: 0}), - defaults: {} - }, - fillCoins: { - effect: (targets, config) => { - // Targets will be all coins from zero to where fill currently is - // Some will already be full, others not. - // Stagger in timeline - // Pulse in size and color - // Shimmer as they shrink back ? - - return U.gsap.effects.throb(targets, {stagger: { - amount: 0.25, - from: "start", - repeat: 1, - yoyo: true - }, ...config ?? {}}); + blurReveal: { + effect: (target, config) => { + return U.gsap.effects.blurRemove(target, config).reverse(0); + }, + defaults: { + ignoreMargin: false, + skewX: -20, + duration: 0.5, + x: "+=300", + scale: 1.5, + filter: "blur(10px)" }, - defaults: { } + extendTimeline: true }, hoverTooltip: { effect: (tooltip, _config) => { @@ -929,7 +893,56 @@ export const gsapEffects: Record = { defaults: { tooltipScale: 0.75 } + }, + textJitter: { + effect: (target, config) => { + const [targetElem] = $(target as HTMLElement); + if (!targetElem) { throw new Error("textJitter effect: target not found"); } + + const split = new SplitText(targetElem, {type: "chars"}); + + return U.gsap.timeline() + .to(targetElem, { + autoAlpha: 1, + duration: config.duration, + ease: "none" + }) + .fromTo(split.chars, { + y: -config.yAmp + }, { + y: config.yAmp, + duration: config.duration, + ease: "sine.inOut", + stagger: { + repeat: -1, + yoyo: true, + from: "random", + each: config.stagger + } as gsap.StaggerVars + }, 0) + .fromTo(split.chars, { + rotateZ: -config.rotateAmp + }, { + rotateZ: config.rotateAmp, + duration: config.duration, + ease: CustomWiggle.create("myWiggle", {wiggles: 10, type: "random"}), + stagger: { + repeat: -1, + from: "random", + yoyo: true, + each: config.stagger + } as gsap.StaggerVars + }, 0); + }, + defaults: { + yAmp: 2, + rotateAmp: 1, + duration: 1, + stagger: 0.05 + }, + extendTimeline: true } + // #endregion }; /** @@ -974,7 +987,7 @@ export function ApplyTooltipAnimations(html: JQuery) { html.find(".tooltip-trigger").each((_, el) => { const tooltipElem = $(el).find(".tooltip")[0] ?? $(el).next(".tooltip")[0]; - if (!tooltipElem) { return; } + if (!tooltipElem) {return;} // Find the tooltip's parent container. If its position isn't relative or absolute, set it to relative. const tooltipContainer = $(tooltipElem).parent()[0]; diff --git a/ts/core/helpers.ts b/ts/core/helpers.ts index da1fdc2a..9bd8809c 100644 --- a/ts/core/helpers.ts +++ b/ts/core/helpers.ts @@ -21,7 +21,6 @@ export async function preloadHandlebarsTemplates() { "systems/eunos-blades/templates/components/portrait.hbs", "systems/eunos-blades/templates/components/clock.hbs", "systems/eunos-blades/templates/components/roll-collab-mod.hbs", - "systems/eunos-blades/templates/components/roll-collab-opposition.hbs", "systems/eunos-blades/templates/components/slide-out-controls.hbs", "systems/eunos-blades/templates/components/consequence.hbs", "systems/eunos-blades/templates/components/consequence-accepted.hbs", diff --git a/ts/core/utilities.ts b/ts/core/utilities.ts index a0d7fac7..c97db0d3 100644 --- a/ts/core/utilities.ts +++ b/ts/core/utilities.ts @@ -1081,8 +1081,19 @@ function objMap|unknown[]>( return [(keyFuncTyped as mapFunc)(key, val), valFuncTyped(val, key)]; })); } -const objSize = (obj: Index) => Object.values(obj).filter((val) => val !== undefined && val !== null).length; - +/** + * This function returns the 'size' of any reference passed into it, following these rules: + * - object: the number of enumerable keys + * - array: the number of elements + * - false/null/undefined: 0 + * - anything else: 1 + */ +const objSize = (obj: unknown) => { + if (isSimpleObj(obj)) { return Object.keys(obj).length; } + if (isArray(obj)) { return obj.length; } + if (obj === false || obj === null || obj === undefined) { return 0; } + return 1; +}; /** * This function is an object-equivalent of Array.findIndex() function. @@ -1499,45 +1510,6 @@ const getSvgPaths = (svgDotKey: string, svgPathKeys?: string|string[]): Record gsap.set(targets, vars); -function get(target: gsap.TweenTarget, property: keyof gsap.CSSProperties & string, unit: string): number; -function get(target: gsap.TweenTarget, property: keyof gsap.CSSProperties & string): string | number; -/** - * - * @param target - * @param property - * @param unit - */ -function get(target: gsap.TweenTarget, property: keyof gsap.CSSProperties & string, unit?: string): string | number { - if (unit) { - const propVal = regExtract(gsap.getProperty(target, property, unit), /[\d.]+/); - if (typeof propVal === "string") { - return pFloat(propVal); - } - throw new Error(`Unable to extract property '${property}' in '${unit}' units from ${target}`); - } - return gsap.getProperty(target, property); -} - -const getGSAngleDelta = (startAngle: number, endAngle: number) => signNum(roundNum(getAngleDelta(startAngle, endAngle), 2)).replace(/^(.)/, "$1="); - -// const Animate = { -// Timeline: { -// to: (tl: gsap.core.Timeline, targets: gsap.TweenTarget[], vars: gsap.TweenVars, position: any) => { -// if (targets.length === 0) { - -// } -// } -// } (tl: gsap.core.Timeline, ) -// } - -// const to = (targets: gsap.TweenTarget[], vars: gsap.TweenVars): gsap.core.Tween => { -// gsap. -// } -// #endregion ░░░░[GreenSock]░░░░ - // #region ░░░░░░░[SVG]░░░░ SVG Generation & Manipulation ░░░░░░░ ~ const getRawCirclePath = (r: number, {x: xO, y: yO}: Point = {x: 0, y: 0}): Array> => { [r, xO, yO] = [r, xO, yO].map((val) => roundNum(val, 2)); @@ -1660,6 +1632,72 @@ const escapeHTML = (str: T): T => (typeof str === "string" .replace(/[`']/g, "'") as T : str); + +// #region ░░░░░░░[GreenSock]░░░░ Wrappers for GreenSock Functions ░░░░░░░ ~ +const set = (targets: gsap.TweenTarget, vars: gsap.TweenVars): gsap.core.Tween => gsap.set(targets, vars); +function get(target: gsap.TweenTarget, property: keyof gsap.CSSProperties & string, unit: string): number; +function get(target: gsap.TweenTarget, property: keyof gsap.CSSProperties & string): string | number; +/** + * + * @param target + * @param property + * @param unit + */ +function get(target: gsap.TweenTarget, property: keyof gsap.CSSProperties & string, unit?: string): string | number { + if (unit) { + const propVal = regExtract(gsap.getProperty(target, property, unit), /[\d.]+/); + if (typeof propVal === "string") { + return pFloat(propVal); + } + throw new Error(`Unable to extract property '${property}' in '${unit}' units from ${target}`); + } + return gsap.getProperty(target, property); +} + +const getGSAngleDelta = (startAngle: number, endAngle: number) => signNum(roundNum(getAngleDelta(startAngle, endAngle), 2)).replace(/^(.)/, "$1="); + +const getNearestLabel = (tl: gsap.core.Timeline, matchTest?: RegExp|string): string|undefined => { + if (!tl) { return undefined; } + if (!objSize(tl.labels)) { return undefined; } + if (typeof matchTest === "string") { + matchTest = new RegExp(matchTest); + } + + // Filter the labels against the matchTest, if one provided, and sort by time in ascending order. + const labelTimes = Object.entries(tl.labels) + .filter(([label]) => { + return matchTest instanceof RegExp + ? matchTest.test(label) + : true; + }) + .sort((a, b) => a[1] - b[1]); + + // Snap the current time of the timeline to the values in labelTimes + const nearestTime = gsap.utils.snap(labelTimes.map(([_label, time]) => time), tl.time()); + + // Get the associated label for the nearest time + const [nearestLabel] = labelTimes.find(([_label, time]) => time === nearestTime) as [string, number]; + + return nearestLabel; +}; + +const reverseRepeatingTimeline = (tl: gsap.core.Timeline) => { + // FIRST: Determine if timeline itself is repeating, or if most-recent child tween of timeline is repeating + if (tl.repeat() === -1) { + // Timeline itself is repeating. Set totalTime equal to time, reverse. + tl.totalTime(tl.time()); + } else { + // Get currently-running child tween, check if that is repeating. + const [tw] = tl.getChildren(false, true, true, tl.time()); + if (tw && tw.repeat() === -1) { + // Child tween is repeating. Set totalTime of TWEEN equal to time, reverse TIMELINE. + tw.totalTime(tw.time()); + } + tl.reverse(); + } +}; +// #endregion ░░░░[GreenSock]░░░░ + // #endregion ▄▄▄▄▄ HTML ▄▄▄▄▄ // #region ████████ ASYNC: Async Functions, Asynchronous Flow Control ████████ ~ @@ -1880,11 +1918,6 @@ export default { getSvgCode, getSvgPaths, changeContainer, - // ░░░░░░░ GreenSock ░░░░░░░ - gsap, get, set, getGSAngleDelta, /* to, from, fromTo, */ - - TextPlugin, Flip, MotionPathPlugin, - getRawCirclePath, drawCirclePath, getColorVals, getRGBString, getHEXString, getContrastingColor, getRandomColor, @@ -1893,6 +1926,11 @@ export default { escapeHTML, + // ░░░░░░░ GreenSock ░░░░░░░ + gsap, get, set, getGSAngleDelta, getNearestLabel, reverseRepeatingTimeline, /* to, from, fromTo, */ + + TextPlugin, Flip, MotionPathPlugin, + // ████████ ASYNC: Async Functions, Asynchronous Flow Control ████████ sleep, diff --git a/ts/sheets/item/BladesClockKeeperSheet.ts b/ts/sheets/item/BladesClockKeeperSheet.ts index 59658289..cdddb5b6 100644 --- a/ts/sheets/item/BladesClockKeeperSheet.ts +++ b/ts/sheets/item/BladesClockKeeperSheet.ts @@ -57,8 +57,9 @@ class BladesClockKeeperSheet extends BladesItemSheet { override async activateListeners(html: JQuery) { super.activateListeners(html); - function getClockKeyFromEvent(event: ClickEvent): BladesClockKey { - const id = $(event.currentTarget).data("keyId"); + function getClockKeyFromEvent(event: ClickEvent|ChangeEvent): BladesClockKey { + const id = $(event.currentTarget).data("keyId") + || $(event.currentTarget).closest(".clock-key-control-flipper").data("clockKeyId"); if (!id) { throw new Error("No id found on element"); } const clockKey = game.eunoblades.ClockKeys.get(id as IDString); if (!clockKey) { throw new Error(`Clock key with id ${id} not found`); } @@ -101,6 +102,29 @@ class BladesClockKeeperSheet extends BladesItemSheet { await getClockKeyFromEvent(event).pull_SocketCall(); } }); + + html.find("[data-action=\"toggle-name-visibility\"]").on({ + click: async (event: ClickEvent) => { + event.preventDefault(); + const clockKey = getClockKeyFromEvent(event); + clockKey.updateTarget("isNameVisible", !clockKey.isNameVisible); + + // If clockKey is in this scene and isVisible, must send out socket calls for animating name fading in/out + if (clockKey.isInCurrentScene && clockKey.isVisible) { + if (clockKey.isNameVisible) { + clockKey.fadeOutName_SocketCall(); + } else { + clockKey.fadeInName_SocketCall(); + } + } + } + }); + + html.find("input.clock-key-input:not([readonly])").on({change: async (event: ChangeEvent) => { + const input$ = $(event.currentTarget); + await getClockKeyFromEvent(event).updateTarget(input$.data("targetProp"), input$.val()); + }}); + } } diff --git a/ts/sheets/item/BladesItemSheet.ts b/ts/sheets/item/BladesItemSheet.ts index 58839309..26863252 100644 --- a/ts/sheets/item/BladesItemSheet.ts +++ b/ts/sheets/item/BladesItemSheet.ts @@ -236,9 +236,6 @@ class BladesItemSheet extends ItemSheet { }, [BladesItemType.project]: (context) => { if (!(this.item instanceof BladesProject)) {return undefined as never;} - // if (this.item.clockKey) { - // this.item.clockKey.isShowingControls = game.user.isGM; - // } const sheetData: BladesItemDataOfType = {}; return { ...context,