|
|
- import { FILTERS } from '../../applications/overlayConfig.js';
- import { evaluateComparator, getTokenEffects } from '../hooks/effectMappingHooks.js';
- import {
- registerOverlayRefreshHook,
- unregisterOverlayRefreshHooks,
- } from '../hooks/overlayHooks.js';
- import { DEFAULT_OVERLAY_CONFIG } from '../models.js';
- import { interpolateColor, removeMarkedOverlays } from '../token/overlay.js';
- import { executeMacro, toggleCEEffect, toggleTMFXPreset, tv_executeScript } from '../utils.js';
-
- class OutlineFilter extends OutlineOverlayFilter {
- /** @inheritdoc */
- static createFragmentShader() {
- return `
- varying vec2 vTextureCoord;
- varying vec2 vFilterCoord;
- uniform sampler2D uSampler;
-
- uniform vec2 thickness;
- uniform vec4 outlineColor;
- uniform vec4 filterClamp;
- uniform float alphaThreshold;
- uniform float time;
- uniform bool knockout;
- uniform bool wave;
-
- ${this.CONSTANTS}
- ${this.WAVE()}
-
- void main(void) {
- float dist = distance(vFilterCoord, vec2(0.5)) * 2.0;
- vec4 ownColor = texture2D(uSampler, vTextureCoord);
- vec4 wColor = wave ? outlineColor *
- wcos(0.0, 1.0, dist * 75.0,
- -time * 0.01 + 3.0 * dot(vec4(1.0), ownColor))
- * 0.33 * (1.0 - dist) : vec4(0.0);
- float texAlpha = smoothstep(alphaThreshold, 1.0, ownColor.a);
- vec4 curColor;
- float maxAlpha = 0.;
- vec2 displaced;
- for ( float angle = 0.0; angle <= TWOPI; angle += ${this.#quality.toFixed(7)} ) {
- displaced.x = vTextureCoord.x + thickness.x * cos(angle);
- displaced.y = vTextureCoord.y + thickness.y * sin(angle);
- curColor = texture2D(uSampler, clamp(displaced, filterClamp.xy, filterClamp.zw));
- curColor.a = clamp((curColor.a - 0.6) * 2.5, 0.0, 1.0);
- maxAlpha = max(maxAlpha, curColor.a);
- }
- float resultAlpha = max(maxAlpha, texAlpha);
- vec3 result = (ownColor.rgb + outlineColor.rgb * (1.0 - texAlpha)) * resultAlpha;
- gl_FragColor = vec4((ownColor.rgb + outlineColor.rgb * (1. - ownColor.a)) * resultAlpha, resultAlpha);
- }
- `;
- }
-
- static get #quality() {
- switch (canvas.performance.mode) {
- case CONST.CANVAS_PERFORMANCE_MODES.LOW:
- return (Math.PI * 2) / 10;
- case CONST.CANVAS_PERFORMANCE_MODES.MED:
- return (Math.PI * 2) / 20;
- default:
- return (Math.PI * 2) / 30;
- }
- }
- }
-
- export class TVASprite extends TokenMesh {
- constructor(pTexture, token, config) {
- super(token);
- if (pTexture.shapes) pTexture.shapes = this.addChild(pTexture.shapes);
- this.pseudoTexture = pTexture;
- this.texture = pTexture.texture;
- //this.setTexture(pTexture, { refresh: false });
-
- this.ready = false;
- this.overlaySort = 0;
-
- this.overlayConfig = mergeObject(DEFAULT_OVERLAY_CONFIG, config, { inplace: false });
-
- // linkDimensions has been converted to linkDimensionsX and linkDimensionsY
- // Make sure we're using the latest fields
- // 20/07/2023
- if (!('linkDimensionsX' in this.overlayConfig) && this.overlayConfig.linkDimensions) {
- this.overlayConfig.linkDimensionsX = true;
- this.overlayConfig.linkDimensionsY = true;
- }
-
- this._registerHooks(this.overlayConfig);
- this._tvaPlay().then(() => this.refresh());
-
- // Workaround needed for v11 visible property
- Object.defineProperty(this, 'visible', {
- get: this._customVisible,
- set: function () {},
- configurable: true,
- });
-
- this.enableInteractivity(this.overlayConfig);
- }
-
- enableInteractivity() {
- if (this.mouseInteractionManager && !this.overlayConfig.interactivity?.length) {
- this.removeAllListeners();
- this.mouseInteractionManager = null;
- this.cursor = null;
- return;
- } else if (this.mouseInteractionManager || !this.overlayConfig.interactivity?.length) return;
-
- if (canvas.primary.eventMode === 'none') {
- canvas.primary.eventMode = 'passive';
- }
-
- this.eventMode = 'static';
- this.cursor = 'pointer';
- const token = this.object;
- const sprite = this;
-
- const runInteraction = function (event, listener) {
- sprite.overlayConfig.interactivity.forEach((i) => {
- if (i.listener === listener) {
- event.preventDefault();
- event.stopPropagation();
- if (i.script) tv_executeScript(i.script, { token });
- if (i.macro) executeMacro(i.macro, token);
- if (i.ceEffect) toggleCEEffect(token, i.ceEffect);
- if (i.tmfxPreset) toggleTMFXPreset(token, i.tmfxPreset);
- }
- });
- };
-
- const permissions = {
- hoverIn: () => true,
- hoverOut: () => true,
- clickLeft: () => true,
- clickLeft2: () => true,
- clickRight: () => true,
- clickRight2: () => true,
- dragStart: () => false,
- };
-
- const callbacks = {
- hoverIn: (event) => runInteraction(event, 'hoverIn'),
- hoverOut: (event) => runInteraction(event, 'hoverOut'),
- clickLeft: (event) => runInteraction(event, 'clickLeft'),
- clickLeft2: (event) => runInteraction(event, 'clickLeft2'),
- clickRight: (event) => runInteraction(event, 'clickRight'),
- clickRight2: (event) => runInteraction(event, 'clickRight2'),
- dragLeftStart: null,
- dragLeftMove: null,
- dragLeftDrop: null,
- dragLeftCancel: null,
- dragRightStart: null,
- dragRightMove: null,
- dragRightDrop: null,
- dragRightCancel: null,
- longPress: null,
- };
-
- const options = { target: null };
-
- // Create the interaction manager
- const mgr = new MouseInteractionManager(this, canvas.stage, permissions, callbacks, options);
- this.mouseInteractionManager = mgr.activate();
- }
-
- _customVisible() {
- const ov = this.overlayConfig;
- if (!this.ready || !(this.object.visible || ov.alwaysVisible)) return false;
-
- if (ov.limitedToOwner && !this.object.owner) return false;
- if (ov.limitedUsers?.length && !ov.limitedUsers.includes(game.user.id)) return false;
-
- if (ov.limitOnEffect || ov.limitOnProperty) {
- const speaker = ChatMessage.getSpeaker();
- let token = canvas.ready ? canvas.tokens.get(speaker.token) : null;
- if (!token) return false;
- if (ov.limitOnEffect) {
- if (!getTokenEffects(token).includes(ov.limitOnEffect)) return false;
- }
- if (ov.limitOnProperty) {
- if (!evaluateComparator(token.document, ov.limitOnProperty)) return false;
- }
- }
-
- if (ov.limitOnHover || ov.limitOnControl || ov.limitOnHighlight) {
- let visible = false;
- if (
- ov.limitOnHover &&
- canvas.controls.ruler._state === Ruler.STATES.INACTIVE &&
- this.object.hover
- )
- visible = true;
- if (ov.limitOnControl && this.object.controlled) visible = true;
- if (ov.limitOnHighlight && (canvas.tokens.highlightObjects ?? canvas.tokens._highlight))
- visible = true;
- return visible;
- }
- return true;
- }
-
- // Overlays have the same sort order as the parent
- get sort() {
- let sort = this.object.document.sort || 0;
- if (this.overlayConfig.top) return sort + 1000;
- else if (this.overlayConfig.bottom) return sort - 1000;
- return sort;
- }
-
- get _lastSortedIndex() {
- return (this.object.mesh._lastSortedIndex || 0) + this.overlaySort;
- }
-
- set _lastSortedIndex(val) {}
-
- async _tvaPlay() {
- // Ensure playback state for video
- const source = foundry.utils.getProperty(this.texture, 'baseTexture.resource.source');
- if (source && source.tagName === 'VIDEO') {
- // Detach video from others
- const s = source.cloneNode();
-
- if (this.overlayConfig.playOnce) {
- s.onended = () => {
- this.alpha = 0;
- this.tvaVideoEnded = true;
- };
- }
-
- await new Promise((resolve) => (s.oncanplay = resolve));
- this.texture = PIXI.Texture.from(s, { resourceOptions: { autoPlay: false } });
-
- const options = {
- loop: this.overlayConfig.loop && !this.overlayConfig.playOnce,
- volume: 0,
- offset: 0,
- playing: true,
- };
- game.video.play(s, options);
- }
- }
-
- addChildAuto(...children) {
- if (this.pseudoTexture?.shapes) {
- return this.pseudoTexture.shapes.addChild(...children);
- } else {
- return this.addChild(...children);
- }
- }
-
- setTexture(pTexture, { preview = false, refresh = true, configuration = null } = {}) {
- // Text preview handling
- if (preview) {
- this._swapChildren(pTexture);
- if (this.originalTexture) this._destroyTexture();
- else {
- this.originalTexture = this.pseudoTexture;
- if (this.originalTexture.shapes) this.removeChild(this.originalTexture.shapes);
- }
- this.pseudoTexture = pTexture;
- this.texture = pTexture.texture;
- if (pTexture.shapes) pTexture.shapes = this.addChild(pTexture.shapes);
- } else if (this.originalTexture) {
- this._swapChildren(this.originalTexture);
- this._destroyTexture();
- this.pseudoTexture = this.originalTexture;
- this.texture = this.originalTexture.texture;
- if (this.originalTexture.shapes)
- this.pseudoTexture.shapes = this.addChild(this.originalTexture.shapes);
- delete this.originalTexture;
- } else {
- this._swapChildren(pTexture);
- this._destroyTexture();
- this.pseudoTexture = pTexture;
- this.texture = pTexture.texture;
- if (pTexture.shapes) this.pseudoTexture.shapes = this.addChild(pTexture.shapes);
- }
-
- if (refresh) this.refresh(configuration, { fullRefresh: !preview });
- }
-
- refresh(configuration, { preview = false, fullRefresh = true, previewTexture = null } = {}) {
- if (!this.overlayConfig || !this.texture) return;
-
- // Text preview handling
- if (previewTexture || this.originalTexture) {
- this.setTexture(previewTexture, { preview: Boolean(previewTexture), refresh: false });
- }
-
- // Register/Unregister hooks that should refresh this overlay
- if (configuration) {
- this._registerHooks(configuration);
- }
-
- const config = mergeObject(this.overlayConfig, configuration ?? {}, { inplace: !preview });
-
- this.enableInteractivity(config);
-
- if (fullRefresh) {
- const source = foundry.utils.getProperty(this.texture, 'baseTexture.resource.source');
- if (source && source.tagName === 'VIDEO') {
- if (!source.loop && config.loop) {
- game.video.play(source);
- } else if (source.loop && !config.loop) {
- game.video.stop(source);
- }
- source.loop = config.loop;
- }
- }
-
- const shapes = this.pseudoTexture.shapes;
-
- // Scale the image using the same logic as the token
- const dimensions = shapes ?? this.texture;
- if (config.linkScale && !config.parentID) {
- const scale = this.scale;
- const aspect = dimensions.width / dimensions.height;
- if (aspect >= 1) {
- scale.x = (this.object.w * this.object.document.texture.scaleX) / dimensions.width;
- scale.y = Number(scale.x);
- } else {
- scale.y = (this.object.h * this.object.document.texture.scaleY) / dimensions.height;
- scale.x = Number(scale.y);
- }
- } else if (config.linkStageScale) {
- this.scale.x = 1 / canvas.stage.scale.x;
- this.scale.y = 1 / canvas.stage.scale.y;
- } else if (config.linkDimensionsX || config.linkDimensionsY) {
- if (config.linkDimensionsX) {
- this.scale.x = this.object.document.width;
- }
- if (config.linkDimensionsY) {
- this.scale.y = this.object.document.height;
- }
- } else {
- this.scale.x = config.width ? config.width / dimensions.width : 1;
- this.scale.y = config.height ? config.height / dimensions.height : 1;
- }
-
- // Adjust scale according to config
- this.scale.x = this.scale.x * config.scaleX;
- this.scale.y = this.scale.y * config.scaleY;
-
- // Check if mirroring should be inherited from the token and if so apply it
- if (config.linkMirror && !config.parentID) {
- this.scale.x = Math.abs(this.scale.x) * (this.object.document.texture.scaleX < 0 ? -1 : 1);
- this.scale.y = Math.abs(this.scale.y) * (this.object.document.texture.scaleY < 0 ? -1 : 1);
- }
-
- if (this.anchor) {
- if (!config.anchor) this.anchor.set(0.5, 0.5);
- else this.anchor.set(config.anchor.x, config.anchor.y);
- }
-
- let xOff = 0;
- let yOff = 0;
- if (shapes) {
- shapes.position.x = -this.anchor.x * shapes.width;
- shapes.position.y = -this.anchor.y * shapes.height;
- if (config.animation.relative) {
- this.pivot.set(0, 0);
- shapes.pivot.set(
- (0.5 - this.anchor.x) * shapes.width,
- (0.5 - this.anchor.y) * shapes.height
- );
- xOff = shapes.pivot.x * this.scale.x;
- yOff = shapes.pivot.y * this.scale.y;
- }
- } else if (config.animation.relative) {
- xOff = (0.5 - this.anchor.x) * this.width;
- yOff = (0.5 - this.anchor.y) * this.height;
- this.pivot.set(
- (0.5 - this.anchor.x) * this.texture.width,
- (0.5 - this.anchor.y) * this.texture.height
- );
- }
-
- // Position
- if (config.parentID) {
- const anchor = this.parent.anchor ?? { x: 0, y: 0 };
- const pWidth = this.parent.width / this.parent.scale.x;
- const pHeight = this.parent.height / this.parent.scale.y;
- this.position.set(
- -config.offsetX * pWidth - anchor.x * pWidth + pWidth / 2,
- -config.offsetY * pHeight - anchor.y * pHeight + pHeight / 2
- );
- } else {
- if (config.animation.relative) {
- this.position.set(
- this.object.document.x + this.object.w / 2 + -config.offsetX * this.object.w + xOff,
- this.object.document.y + this.object.h / 2 + -config.offsetY * this.object.h + yOff
- );
- } else {
- this.position.set(
- this.object.document.x + this.object.w / 2,
- this.object.document.y + this.object.h / 2
- );
- this.pivot.set(
- (config.offsetX * this.object.w) / this.scale.x,
- (config.offsetY * this.object.h) / this.scale.y
- );
- }
- }
-
- // Set alpha but only if playOnce is disabled and the video hasn't
- // finished playing yet. Otherwise we want to keep alpha as 0 to keep the video hidden
- if (!this.tvaVideoEnded) {
- this.alpha = config.linkOpacity ? this.object.document.alpha : config.alpha;
- }
-
- // Angle in degrees
- if (fullRefresh) {
- if (config.linkRotation) this.angle = this.object.document.rotation + config.angle;
- else this.angle = config.angle;
- } else if (!config.animation.rotate) {
- if (config.linkRotation) this.angle = this.object.document.rotation + config.angle;
- }
-
- // Apply color tinting
- const tint = config.inheritTint
- ? this.object.document.texture.tint
- : interpolateColor(config.tint, config.interpolateColor, true);
- this.tint = tint ? Color.from(tint) : 0xffffff;
-
- if (fullRefresh) {
- if (config.animation.rotate) {
- this.animate(config);
- } else {
- this.stopAnimation();
- }
- }
-
- // Apply filters
- if (fullRefresh) this._applyFilters(config);
- //if (fullRefresh) this.filters = this._getFilters(config);
-
- if (preview && this.children) {
- this.children.forEach((ch) => {
- if (ch instanceof TVASprite) ch.refresh(null, { preview: true });
- });
- }
-
- this.ready = true;
- }
-
- _activateTicker() {
- this._deactivateTicker();
- canvas.app.ticker.add(this.updatePosition, this, PIXI.UPDATE_PRIORITY.HIGH);
- }
-
- _deactivateTicker() {
- canvas.app.ticker.remove(this.updatePosition, this);
- }
-
- updatePosition() {
- let coord = canvas.canvasCoordinatesFromClient({
- x: window.innerWidth / 2 + this.overlayConfig.offsetX * window.innerWidth,
- y: window.innerHeight / 2 + this.overlayConfig.offsetY * window.innerHeight,
- });
- this.position.set(coord.x, coord.y);
- }
-
- async _applyFilters(config) {
- const filterName = config.filter;
- const FilterClass = PIXI.filters[filterName];
- const options = mergeObject(FILTERS[filterName]?.defaultValues || {}, config.filterOptions);
- let filter;
- if (FilterClass) {
- if (FILTERS[filterName]?.argType === 'args') {
- let args = [];
- const controls = FILTERS[filterName]?.controls;
- if (controls) {
- controls.forEach((c) => args.push(options[c.name]));
- }
- filter = new FilterClass(...args);
- } else if (FILTERS[filterName]?.argType === 'options') {
- filter = new FilterClass(options);
- } else {
- filter = new FilterClass();
- }
- } else if (filterName === 'OutlineOverlayFilter') {
- filter = OutlineFilter.create(options);
- filter.thickness = options.trueThickness ?? 1;
- filter.animate = options.animate ?? false;
- } else if (filterName === 'Token Magic FX') {
- this.filters = await constructTMFXFilters(options.params || [], this);
- return;
- }
-
- if (filter) {
- this.filters = [filter];
- } else {
- this.filters = [];
- }
- }
-
- async stopAnimation() {
- if (this.animationName) {
- CanvasAnimation.terminateAnimation(this.animationName);
- }
- }
-
- async animate(config) {
- if (!this.animationName) this.animationName = this.object.sourceId + '.' + randomID(5);
-
- let newAngle = this.angle + (config.animation.clockwise ? 360 : -360);
- const rotate = [{ parent: this, attribute: 'angle', to: newAngle }];
-
- const completed = await CanvasAnimation.animate(rotate, {
- duration: config.animation.duration,
- name: this.animationName,
- });
- if (completed) {
- this.animate(config);
- }
- }
-
- _registerHooks(configuration) {
- if (configuration.linkStageScale) registerOverlayRefreshHook(this, 'canvasPan');
- else unregisterOverlayRefreshHooks(this, 'canvasPan');
- }
-
- _swapChildren(to) {
- const from = this.pseudoTexture;
- if (from.shapes) {
- this.removeChild(this.pseudoTexture.shapes);
- const children = from.shapes.removeChildren();
- if (to?.shapes) children.forEach((c) => to.shapes.addChild(c)?.refresh());
- else children.forEach((c) => this.addChild(c)?.refresh());
- } else if (to?.shapes) {
- const children = this.removeChildren();
- children.forEach((c) => to.shapes.addChild(c)?.refresh());
- }
- }
-
- _destroyTexture() {
- if (this.texture.textLabel || this.texture.destroyable) {
- this.texture.destroy(true);
- }
- if (this.pseudoTexture?.shapes) {
- this.removeChild(this.pseudoTexture.shapes);
- this.pseudoTexture.shapes.destroy();
- }
- }
-
- destroy() {
- this.stopAnimation();
- unregisterOverlayRefreshHooks(this);
-
- if (this.children) {
- for (const ch of this.children) {
- if (ch instanceof TVASprite) ch.tvaRemove = true;
- }
- removeMarkedOverlays(this.object);
- if (this.pseudoTexture.shapes) {
- this.pseudoTexture.shapes.children.forEach((c) => c.destroy());
- this.removeChild(this.pseudoTexture.shapes)?.destroy();
- // this.pseudoTexture.shapes.destroy();
- }
- }
-
- if (this.texture.textLabel || this.texture.destroyable) {
- return super.destroy(true);
- } else if (this.texture?.baseTexture.resource?.source?.tagName === 'VIDEO') {
- this.texture.baseTexture.destroy();
- }
- super.destroy();
- }
-
- // Foundry BUG Fix
- calculateTrimmedVertices() {
- return PIXI.Sprite.prototype.calculateTrimmedVertices.call(this);
- }
- }
-
- async function constructTMFXFilters(paramsArray, sprite) {
- if (typeof TokenMagic === 'undefined') return [];
-
- try {
- paramsArray = eval(paramsArray);
- } catch (e) {
- return [];
- }
-
- if (!Array.isArray(paramsArray)) {
- paramsArray = TokenMagic.getPreset(paramsArray);
- }
- if (!(paramsArray instanceof Array && paramsArray.length > 0)) return [];
-
- let filters = [];
- for (const params of paramsArray) {
- if (
- !params.hasOwnProperty('filterType') ||
- !TMFXFilterTypes.hasOwnProperty(params.filterType)
- ) {
- // one invalid ? all rejected.
- return [];
- }
-
- if (!params.hasOwnProperty('rank')) {
- params.rank = 5000;
- }
-
- if (!params.hasOwnProperty('filterId') || params.filterId == null) {
- params.filterId = randomID();
- }
-
- if (!params.hasOwnProperty('enabled') || !(typeof params.enabled === 'boolean')) {
- params.enabled = true;
- }
-
- params.filterInternalId = randomID();
-
- const gms = game.users.filter((user) => user.isGM);
- params.filterOwner = gms.length ? gms[0].id : game.data.userId;
- // params.placeableType = placeable._TMFXgetPlaceableType();
- params.updateId = randomID();
-
- const filterClass = await getTMFXFilter(params.filterType);
- if (filterClass) {
- filterClass.prototype.assignPlaceable = function () {
- this.targetPlaceable = sprite.object;
- this.placeableImg = sprite;
- };
-
- filterClass.prototype._TMFXsetAnimeFlag = async function () {};
-
- const filter = new filterClass(params);
- if (filter) {
- // Patch fixes
- filter.placeableImg = sprite;
- filter.targetPlaceable = sprite.object;
- // end of fixes
- filters.unshift(filter);
- }
- }
- }
- return filters;
- }
-
- async function getTMFXFilter(id) {
- if (id in TMFXFilterTypes) {
- if (id in LOADED_TMFXFilters) return LOADED_TMFXFilters[id];
- else {
- try {
- const className = TMFXFilterTypes[id];
- let fxModule = await import(`../../../tokenmagic/fx/filters/${className}.js`);
- if (fxModule && fxModule[className]) {
- LOADED_TMFXFilters[id] = fxModule[className];
- return fxModule[className];
- }
- } catch (e) {}
- }
- }
- return null;
- }
-
- const LOADED_TMFXFilters = {};
-
- const TMFXFilterTypes = {
- adjustment: 'FilterAdjustment',
- distortion: 'FilterDistortion',
- oldfilm: 'FilterOldFilm',
- glow: 'FilterGlow',
- outline: 'FilterOutline',
- bevel: 'FilterBevel',
- xbloom: 'FilterXBloom',
- shadow: 'FilterDropShadow',
- twist: 'FilterTwist',
- zoomblur: 'FilterZoomBlur',
- blur: 'FilterBlur',
- bulgepinch: 'FilterBulgePinch',
- zapshadow: 'FilterRemoveShadow',
- ray: 'FilterRays',
- fog: 'FilterFog',
- xfog: 'FilterXFog',
- electric: 'FilterElectric',
- wave: 'FilterWaves',
- shockwave: 'FilterShockwave',
- fire: 'FilterFire',
- fumes: 'FilterFumes',
- smoke: 'FilterSmoke',
- flood: 'FilterFlood',
- images: 'FilterMirrorImages',
- field: 'FilterForceField',
- xray: 'FilterXRays',
- liquid: 'FilterLiquid',
- xglow: 'FilterGleamingGlow',
- pixel: 'FilterPixelate',
- web: 'FilterSpiderWeb',
- ripples: 'FilterSolarRipples',
- globes: 'FilterGlobes',
- transform: 'FilterTransform',
- splash: 'FilterSplash',
- polymorph: 'FilterPolymorph',
- xfire: 'FilterXFire',
- sprite: 'FilterSprite',
- replaceColor: 'FilterReplaceColor',
- ddTint: 'FilterDDTint',
- };
|