diff --git a/Fade/1.0.0/Fade.js b/Fade/1.0.0/Fade.js new file mode 100644 index 000000000..56211b9d6 --- /dev/null +++ b/Fade/1.0.0/Fade.js @@ -0,0 +1,123 @@ +var API_Meta = API_Meta||{}; +API_Meta.Fade={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; +{try{throw new Error('');}catch(e){API_Meta.Fade.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-3);}} + +// Fade — Smooth opacity fading for Roll20 graphics +// !fade --in[|time] [--all] +// !fade --out[|time] [--all] + +on('ready', () => { + + const version = '1.0.0'; //version number set here + log('-=> Fade v' + version + ' is loaded. Command !fade --|| <--all>.'); + + +on('chat:message', (msg) => { + if (msg.type !== 'api' || !msg.content.startsWith('!fade')) return; + + const player = getObj('player', msg.playerid); + const args = msg.content.split(/\s+--/).slice(1).map(a => a.trim()); //better + + + const FADE_STEPS = 20; + let activeIntervals = []; + + // Parse arguments + let fadeIn = false; + let fadeOut = false; + let fadeTime = 1; + let affectAll = false; + + args.forEach(arg => { + const [keyRaw, valueRaw] = arg.split('|'); + const key = (keyRaw || '').trim().toLowerCase(); + const value = valueRaw ? valueRaw.trim() : undefined; + + if (key === 'in') { + fadeIn = true; + fadeTime = value ? parseFloat(value) : 1; + } else if (key === 'out') { + fadeOut = true; + fadeTime = value ? parseFloat(value) : 1; + } else if (key === 'all') { + affectAll = true; + } + }); + // Defensive checks + if (!fadeIn && !fadeOut) { + sendChat('Fade', `/w "${player.get('displayname')}" You must specify --in or --out.`); + return; + } + + const targetOpacity = fadeIn ? 1.0 : 0.0; +let pageId = + (player && player.get('lastpage')) || + Campaign().get('playerpageid'); + + if (!affectAll && (!msg.selected || msg.selected.length === 0)) { + sendChat('Fade', `/w "${player.get('displayname')}" No graphics selected. Use --all to affect the entire page.`); + return; + } + +if (affectAll && !pageId) { + sendChat('Fade', `/w "${player.get('displayname')}" Could not determine current page.`); + return; +} + + + // Collect target graphics + let targets = affectAll + ? findObjs({ _pageid: pageId, _type: 'graphic' }) || [] + : msg.selected + .map(sel => getObj(sel._type, sel._id)) + .filter(obj => obj && obj.get('type') === 'graphic'); + + if (targets.length === 0) { + sendChat('Fade', `/w "${player.get('displayname')}" No valid graphics found to fade.`); + return; + } + + const stepInterval = (fadeTime * 1000) / FADE_STEPS; + + // Stop any active fades + activeIntervals.forEach(interval => clearInterval(interval)); + activeIntervals = []; + + // Precompute fixed per-graphic fade steps + const fadeData = targets.map(g => { + const start = parseFloat(g.get('baseOpacity')) || 0; + const diff = targetOpacity - start; + return { + g, + start, + step: diff / FADE_STEPS, + currentStep: 0 + }; + }).filter(fd => Math.abs(fd.step) > 0.0001); // skip already at target + + if (fadeData.length === 0) return; + + const intervalId = setInterval(() => { + let done = true; + + fadeData.forEach(fd => { + if (fd.currentStep < FADE_STEPS) { + const newVal = fd.start + fd.step * (fd.currentStep + 1); + fd.g.set('baseOpacity', Math.max(0, Math.min(1, newVal))); + fd.currentStep++; + done = false; + } else { + fd.g.set('baseOpacity', targetOpacity); + } + }); + + if (done) { + clearInterval(intervalId); + activeIntervals = activeIntervals.filter(id => id !== intervalId); + } + }, stepInterval); + + activeIntervals.push(intervalId); +}); +}); +{ try { throw new Error(''); } catch (e) { API_Meta.Fade.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Fade.offset); } } diff --git a/Fade/Fade.js b/Fade/Fade.js new file mode 100644 index 000000000..56211b9d6 --- /dev/null +++ b/Fade/Fade.js @@ -0,0 +1,123 @@ +var API_Meta = API_Meta||{}; +API_Meta.Fade={offset:Number.MAX_SAFE_INTEGER,lineCount:-1}; +{try{throw new Error('');}catch(e){API_Meta.Fade.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-3);}} + +// Fade — Smooth opacity fading for Roll20 graphics +// !fade --in[|time] [--all] +// !fade --out[|time] [--all] + +on('ready', () => { + + const version = '1.0.0'; //version number set here + log('-=> Fade v' + version + ' is loaded. Command !fade --|| <--all>.'); + + +on('chat:message', (msg) => { + if (msg.type !== 'api' || !msg.content.startsWith('!fade')) return; + + const player = getObj('player', msg.playerid); + const args = msg.content.split(/\s+--/).slice(1).map(a => a.trim()); //better + + + const FADE_STEPS = 20; + let activeIntervals = []; + + // Parse arguments + let fadeIn = false; + let fadeOut = false; + let fadeTime = 1; + let affectAll = false; + + args.forEach(arg => { + const [keyRaw, valueRaw] = arg.split('|'); + const key = (keyRaw || '').trim().toLowerCase(); + const value = valueRaw ? valueRaw.trim() : undefined; + + if (key === 'in') { + fadeIn = true; + fadeTime = value ? parseFloat(value) : 1; + } else if (key === 'out') { + fadeOut = true; + fadeTime = value ? parseFloat(value) : 1; + } else if (key === 'all') { + affectAll = true; + } + }); + // Defensive checks + if (!fadeIn && !fadeOut) { + sendChat('Fade', `/w "${player.get('displayname')}" You must specify --in or --out.`); + return; + } + + const targetOpacity = fadeIn ? 1.0 : 0.0; +let pageId = + (player && player.get('lastpage')) || + Campaign().get('playerpageid'); + + if (!affectAll && (!msg.selected || msg.selected.length === 0)) { + sendChat('Fade', `/w "${player.get('displayname')}" No graphics selected. Use --all to affect the entire page.`); + return; + } + +if (affectAll && !pageId) { + sendChat('Fade', `/w "${player.get('displayname')}" Could not determine current page.`); + return; +} + + + // Collect target graphics + let targets = affectAll + ? findObjs({ _pageid: pageId, _type: 'graphic' }) || [] + : msg.selected + .map(sel => getObj(sel._type, sel._id)) + .filter(obj => obj && obj.get('type') === 'graphic'); + + if (targets.length === 0) { + sendChat('Fade', `/w "${player.get('displayname')}" No valid graphics found to fade.`); + return; + } + + const stepInterval = (fadeTime * 1000) / FADE_STEPS; + + // Stop any active fades + activeIntervals.forEach(interval => clearInterval(interval)); + activeIntervals = []; + + // Precompute fixed per-graphic fade steps + const fadeData = targets.map(g => { + const start = parseFloat(g.get('baseOpacity')) || 0; + const diff = targetOpacity - start; + return { + g, + start, + step: diff / FADE_STEPS, + currentStep: 0 + }; + }).filter(fd => Math.abs(fd.step) > 0.0001); // skip already at target + + if (fadeData.length === 0) return; + + const intervalId = setInterval(() => { + let done = true; + + fadeData.forEach(fd => { + if (fd.currentStep < FADE_STEPS) { + const newVal = fd.start + fd.step * (fd.currentStep + 1); + fd.g.set('baseOpacity', Math.max(0, Math.min(1, newVal))); + fd.currentStep++; + done = false; + } else { + fd.g.set('baseOpacity', targetOpacity); + } + }); + + if (done) { + clearInterval(intervalId); + activeIntervals = activeIntervals.filter(id => id !== intervalId); + } + }, stepInterval); + + activeIntervals.push(intervalId); +}); +}); +{ try { throw new Error(''); } catch (e) { API_Meta.Fade.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Fade.offset); } } diff --git a/Fade/readme.md b/Fade/readme.md new file mode 100644 index 000000000..12e37163e --- /dev/null +++ b/Fade/readme.md @@ -0,0 +1,34 @@ +# Fade + +Fade smoothly transitions graphics between 0% and 100% opacity over a specified time. + +--- + +## Commands + +!fade --in| +!fade --out| +!fade --in --all +!fade --out --all + +**** is optional (default: 1). +**--all** affects all graphics on the current page. + +### Examples +!fade --out|5 → Fade selected graphics to 0% over 5 seconds +!fade --in|3 → Fade selected graphics to 100% over 3 seconds +!fade --in --all → Fade in all graphics on the page over 1 second + +--- + +## Features +- All graphics fade simultaneously +- Works on all layers +- Ignores graphics already at target opacity +- Silent operation (no chat spam) + +--- + +## Usage Notes +- If no graphics are selected, `--all` is required. +- Page detection uses the player's last viewed page or the GM's active page. \ No newline at end of file diff --git a/Fade/script.json b/Fade/script.json new file mode 100644 index 000000000..db5cf3c85 --- /dev/null +++ b/Fade/script.json @@ -0,0 +1,14 @@ +{ + "name": "Fade", + "script": "Fade.js", + "version": "1.0.0", + "description": "# Fade\n\nFade smoothly transitions graphics between 0% and 100% opacity over a specified time.\n\n---\n\n## Commands\n\n```\n!fade --in|\n!fade --out|\n!fade --in --all\n!fade --out --all\n```\n\n**** is optional (default: 1).\n**--all** affects all graphics on the current page.\n\n### Examples\n```\n!fade --out|5 → Fade selected graphics to 0% over 5 seconds\n!fade --in|3 → Fade selected graphics to 100% over 3 seconds\n!fade --in --all → Fade in all graphics on the page over 1 second\n```\n\n---\n\n## Features\n- All graphics fade simultaneously\n- Works on all layers\n- Ignores graphics already at target opacity\n- Silent operation (no chat spam)\n\n---\n\n## Usage Notes\n- If no graphics are selected, `--all` is required.\n- Page detection uses the player's last viewed page or the GM's active page.", + "authors": "Keith Curtis", + "roll20userid": "162065", + "dependencies": [], + "modifies": { + "graphic": "write" + }, + "conflicts": [], + "previousversions": [] +} \ No newline at end of file