diff --git a/src/core/MinimalStim.js b/src/core/MinimalStim.js index 215ed44..c20d575 100644 --- a/src/core/MinimalStim.js +++ b/src/core/MinimalStim.js @@ -101,7 +101,7 @@ export class MinimalStim extends PsychObject } else { - this.win._rootContainer.addChild(this._pixi); + this._win.addPixiObject(this._pixi); this.win._drawList.push(this); } } @@ -111,9 +111,9 @@ export class MinimalStim extends PsychObject // from the window container, update it, then put it back: if (this._needUpdate && typeof this._pixi !== "undefined") { - this.win._rootContainer.removeChild(this._pixi); + this._win.removePixiObject(this._pixi); this._updateIfNeeded(); - this.win._rootContainer.addChild(this._pixi); + this._win.addPixiObject(this._pixi); } } } @@ -140,7 +140,7 @@ export class MinimalStim extends PsychObject // if the stimulus has a pixi representation, remove it from the root container: if (typeof this._pixi !== "undefined") { - this._win._rootContainer.removeChild(this._pixi); + this._win.removePixiObject(this._pixi); } } this.status = PsychoJS.Status.STOPPED; diff --git a/src/core/Window.js b/src/core/Window.js index 7455395..b0eeb50 100644 --- a/src/core/Window.js +++ b/src/core/Window.js @@ -26,7 +26,8 @@ import { Logger } from "./Logger.js"; * @param {string} [options.name] the name of the window * @param {boolean} [options.fullscr= false] whether or not to go fullscreen * @param {Color} [options.color= Color('black')] the background color of the window - * @param {number} [options.gamma= 1] sets the delimiter for gamma correction. In other words gamma correction is calculated as pow(rgb, 1/gamma) + * @param {number} [options.gamma= 1] sets the divisor for gamma correction. In other words gamma correction is calculated as pow(rgb, 1/gamma) + * @param {number} [options.contrast= 1] sets the contrast value * @param {string} [options.units= 'pix'] the units of the window * @param {boolean} [options.waitBlanking= false] whether or not to wait for all rendering operations to be done * before flipping @@ -73,7 +74,11 @@ export class Window extends PsychObject this._drawList = []; this._addAttribute("fullscr", fullscr); - this._addAttribute("color", color); + this._addAttribute("color", color, new Color("black"), () => { + if (this._backgroundSprite) { + this._backgroundSprite.tint = color.int; + } + }); this._addAttribute("gamma", gamma, 1, () => { this._adjustmentFilter.gamma = this._gamma; }); @@ -298,6 +303,28 @@ export class Window extends PsychObject this._flipCallbacks.push({ function: flipCallback, arguments: flipCallbackArgs }); } + /** + * Add PIXI.DisplayObject to the container displayed on the scene (window) + * + * @name module:core.Window#addPixiObject + * @function + * @public + */ + addPixiObject (pixiObject) { + this._stimsContainer.addChild(pixiObject); + } + + /** + * Remove PIXI.DisplayObject from the container displayed on the scene (window) + * + * @name module:core.Window#removePixiObject + * @function + * @public + */ + removePixiObject (pixiObject) { + this._stimsContainer.removeChild(pixiObject); + } + /** * Render the stimuli onto the canvas. * @@ -385,9 +412,9 @@ export class Window extends PsychObject { if (stimulus._needUpdate && typeof stimulus._pixi !== "undefined") { - this._rootContainer.removeChild(stimulus._pixi); + this._stimsContainer.removeChild(stimulus._pixi); stimulus._updateIfNeeded(); - this._rootContainer.addChild(stimulus._pixi); + this._stimsContainer.addChild(stimulus._pixi); } } } @@ -432,6 +459,7 @@ export class Window extends PsychObject width: this._size[0], height: this._size[1], backgroundColor: this.color.int, + powerPreference: "high-performance", resolution: window.devicePixelRatio, }); this._renderer.view.style.transform = "translatez(0)"; @@ -441,8 +469,25 @@ export class Window extends PsychObject // we also change the background color of the body since the dialog popup may be longer than the window's height: document.body.style.backgroundColor = this._color.hex; + // filters in PIXI work in a slightly unexpected fashion: + // when setting this._rootContainer.filters, filtering itself + // ignores backgroundColor of this._renderer and in addition to that + // all child elements of this._rootContainer ignore backgroundColor when blending. + // To circumvent that creating a separate PIXI.Sprite that serves as background color. + // Then placing all Stims to a separate this._stimsContainer which hovers on top of + // background sprite so that if we need to move all stims at once, the background sprite + // won't get affected. + this._backgroundSprite = new PIXI.Sprite(PIXI.Texture.WHITE); + this._backgroundSprite.tint = this.color.int; + this._backgroundSprite.width = this._size[0]; + this._backgroundSprite.height = this._size[1]; + this._backgroundSprite.anchor.set(.5); + this._stimsContainer = new PIXI.Container(); + this._stimsContainer.sortableChildren = true; + // create a top-level PIXI container: this._rootContainer = new PIXI.Container(); + this._rootContainer.addChild(this._backgroundSprite, this._stimsContainer); this._rootContainer.interactive = true; this._rootContainer.filters = [this._adjustmentFilter]; diff --git a/src/visual/Form.js b/src/visual/Form.js index 5d7001f..91cb645 100644 --- a/src/visual/Form.js +++ b/src/visual/Form.js @@ -1009,8 +1009,8 @@ export class Form extends util.mix(VisualStim).with(ColorMixin) this._stimuliClipMask.clear(); this._stimuliClipMask.beginFill(0xFFFFFF); this._stimuliClipMask.drawRect( - this._win._rootContainer.position.x + this._leftEdge_px + 2, - this._win._rootContainer.position.y + this._bottomEdge_px + 2, + this._win._stimsContainer.position.x + this._leftEdge_px + 2, + this._win._stimsContainer.position.y + this._bottomEdge_px + 2, this._size_px[0] - 4, this._size_px[1] - 6, ); diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index 88015a0..154523e 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -8,8 +8,8 @@ */ import * as PIXI from "pixi.js-legacy"; +import {AdjustmentFilter} from "@pixi/filter-adjustment"; import { Color } from "../util/Color.js"; -import { ColorMixin } from "../util/ColorMixin.js"; import { to_pixiPoint } from "../util/Pixi.js"; import * as util from "../util/Util.js"; import { VisualStim } from "./VisualStim.js"; @@ -32,7 +32,6 @@ import raisedCosShader from "./shaders/raisedCosShader.frag"; * @name module:visual.GratingStim * @class * @extends VisualStim - * @mixes ColorMixin * @param {Object} options * @param {String} options.name - the name used when logging messages from this stimulus * @param {Window} options.win - the associated Window @@ -44,17 +43,17 @@ import raisedCosShader from "./shaders/raisedCosShader.frag"; * @param {Array.} [options.pos= [0, 0]] - the position of the center of the stimulus * @param {number} [options.ori= 0.0] - the orientation (in degrees) * @param {number} [options.size] - the size of the rendered image (DEFAULT_STIM_SIZE_PX will be used if size is not specified) - * @param {Color} [options.color= "white"] the background color - * @param {number} [options.opacity= 1.0] - the opacity - * @param {number} [options.contrast= 1.0] - the contrast + * @param {Color} [options.color= "white"] - Foreground color of the stimulus. Can be String like "red" or "#ff0000" or Number like 0xff0000. + * @param {number} [options.opacity= 1.0] - Set the opacity of the stimulus. Determines how visible the stimulus is relative to background. + * @param {number} [options.contrast= 1.0] - Set the contrast of the stimulus, i.e. scales how far the stimulus deviates from the middle grey. Ranges [-1, 1]. * @param {number} [options.depth= 0] - the depth (i.e. the z order) * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated. NOT IMPLEMENTED YET. - * @param {String} [options.blendmode= 'avg'] - blend mode of the stimulus, determines how the stimulus is blended with the background. NOT IMPLEMENTED YET. + * @param {String} [options.blendmode= "avg"] - blend mode of the stimulus, determines how the stimulus is blended with the background. Supported values: "avg", "add", "mul", "screen". * @param {boolean} [options.autoDraw= false] - whether or not the stimulus should be automatically drawn on every frame flip * @param {boolean} [options.autoLog= false] - whether or not to log */ -export class GratingStim extends util.mix(VisualStim).with(ColorMixin) +export class GratingStim extends VisualStim { /** * An object that keeps shaders source code and default uniform values for them. @@ -143,21 +142,23 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) uniforms: { uFreq: 1.0, uPhase: 0.0, - uColor: [.5, 0, .5] + uColor: [1., 1., 1.] } }, sqr: { shader: sqrShader, uniforms: { uFreq: 1.0, - uPhase: 0.0 + uPhase: 0.0, + uColor: [1., 1., 1.] } }, saw: { shader: sawShader, uniforms: { uFreq: 1.0, - uPhase: 0.0 + uPhase: 0.0, + uColor: [1., 1., 1.] } }, tri: { @@ -165,27 +166,31 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) uniforms: { uFreq: 1.0, uPhase: 0.0, - uPeriod: 1.0 + uPeriod: 1.0, + uColor: [1., 1., 1.] } }, sinXsin: { shader: sinXsinShader, uniforms: { uFreq: 1.0, - uPhase: 0.0 + uPhase: 0.0, + uColor: [1., 1., 1.] } }, sqrXsqr: { shader: sqrXsqrShader, uniforms: { uFreq: 1.0, - uPhase: 0.0 + uPhase: 0.0, + uColor: [1., 1., 1.] } }, circle: { shader: circleShader, uniforms: { - uRadius: 1.0 + uRadius: 1.0, + uColor: [1., 1., 1.] } }, gauss: { @@ -193,26 +198,30 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) uniforms: { uA: 1.0, uB: 0.0, - uC: 0.16 + uC: 0.16, + uColor: [1., 1., 1.] } }, cross: { shader: crossShader, uniforms: { - uThickness: 0.2 + uThickness: 0.2, + uColor: [1., 1., 1.] } }, radRamp: { shader: radRampShader, uniforms: { - uSqueeze: 1.0 + uSqueeze: 1.0, + uColor: [1., 1., 1.] } }, raisedCos: { shader: raisedCosShader, uniforms: { uBeta: 0.25, - uPeriod: 0.625 + uPeriod: 0.625, + uColor: [1., 1., 1.] } } }; @@ -225,6 +234,13 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) */ static #DEFAULT_STIM_SIZE_PX = [256, 256]; // in pixels + static #BLEND_MODES_MAP = { + avg: PIXI.BLEND_MODES.NORMAL, + add: PIXI.BLEND_MODES.ADD, + mul: PIXI.BLEND_MODES.MULTIPLY, + screen: PIXI.BLEND_MODES.SCREEN + }; + constructor({ name, tex = "sin", @@ -239,7 +255,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) color, colorSpace, opacity, - contrast, + contrast = 1, depth, interpolate, blendmode, @@ -250,36 +266,19 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) { super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog }); - this._addAttribute( - "tex", - tex, - ); - this._addAttribute( - "mask", - mask, - ); - this._addAttribute( - "SF", - sf, - GratingStim.#SHADERS[tex] ? GratingStim.#SHADERS[tex].uniforms.uFreq || 1.0 : 1.0 - ); - this._addAttribute( - "phase", - phase, - GratingStim.#SHADERS[tex] ? GratingStim.#SHADERS[tex].uniforms.uPhase || 0.0 : 0.0 - ); - this._addAttribute( - "color", - color, - "white", - this._onChange(true, false), - ); - this._addAttribute( - "contrast", - contrast, - 1.0, - this._onChange(true, false), - ); + this._adjustmentFilter = new AdjustmentFilter({ + contrast + }); + this._addAttribute("tex", tex); + this._addAttribute("mask", mask); + this._addAttribute("SF", sf, GratingStim.#SHADERS[tex] ? GratingStim.#SHADERS[tex].uniforms.uFreq || 1.0 : 1.0); + this._addAttribute("phase", phase, GratingStim.#SHADERS[tex] ? GratingStim.#SHADERS[tex].uniforms.uPhase || 0.0 : 0.0); + this._addAttribute("color", color, "white"); + this._addAttribute("colorSpace", colorSpace, "RGB"); + this._addAttribute("contrast", contrast, 1.0, () => { + this._adjustmentFilter.contrast = this._contrast; + }); + this._addAttribute("blendmode", blendmode, "avg"); this._addAttribute( "interpolate", interpolate, @@ -521,6 +520,43 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) } } + /** + * Set color space value for the grating stimulus. + * + * @name module:visual.GratingStim#setColorSpace + * @public + * @param {String} colorSpaceVal - color space value + * @param {boolean} [log= false] - whether of not to log + */ + setColorSpace (colorSpaceVal = "RGB", log = false) { + let colorSpaceValU = colorSpaceVal.toUpperCase(); + if (Color.COLOR_SPACE[colorSpaceValU] === undefined) { + colorSpaceValU = "RGB"; + } + const hasChanged = this._setAttribute("colorSpace", colorSpaceValU, log); + if (hasChanged) { + this.setColor(this._color); + } + } + + /** + * Set foreground color value for the grating stimulus. + * + * @name module:visual.GratingStim#setColor + * @public + * @param {Color} colorVal - color value, can be String like "red" or "#ff0000" or Number like 0xff0000. + * @param {boolean} [log= false] - whether of not to log + */ + setColor (colorVal = "white", log = false) { + const colorObj = (colorVal instanceof Color) ? colorVal : new Color(colorVal, Color.COLOR_SPACE[this._colorSpace]) + this._setAttribute("color", colorObj, log); + if (this._pixi instanceof PIXI.Mesh) { + this._pixi.shader.uniforms.uColor = colorObj.rgb; + } else if (this._pixi instanceof PIXI.TilingSprite) { + + } + } + /** * Set spatial frequency value for the function. * @@ -543,6 +579,29 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) } } + /** + * Set blend mode of the grating stimulus. + * + * @name module:visual.GratingStim#setBlendmode + * @public + * @param {String} blendMode - blend mode, can be one of the following: ["avg", "add", "mul", "screen"]. + * @param {boolean} [log=false] - whether or not to log + */ + setBlendmode (blendMode = "avg", log = false) { + this._setAttribute("blendmode", blendMode, log); + if (this._pixi !== undefined) { + let pixiBlendMode = GratingStim.#BLEND_MODES_MAP[blendMode]; + if (pixiBlendMode === undefined) { + pixiBlendMode = PIXI.BLEND_MODES.NORMAL; + } + if (this._pixi.filters) { + this._pixi.filters[this._pixi.filters.length - 1].blendMode = pixiBlendMode; + } else { + this._pixi.blendMode = pixiBlendMode; + } + } + } + /** * Update the stimulus, if necessary. * @@ -590,6 +649,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) }); } this._pixi.pivot.set(this._pixi.width * 0.5, this._pixi.width * 0.5); + this._pixi.filters = [this._adjustmentFilter]; // add a mask if need be: if (typeof this._mask !== "undefined") diff --git a/src/visual/shaders/circleShader.frag b/src/visual/shaders/circleShader.frag index 51e9ecc..6180176 100644 --- a/src/visual/shaders/circleShader.frag +++ b/src/visual/shaders/circleShader.frag @@ -16,9 +16,10 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uRadius; +uniform vec3 uColor; void main() { vec2 uv = vUvs; float s = 1. - step(uRadius, length(uv * 2. - 1.)); - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/crossShader.frag b/src/visual/shaders/crossShader.frag index b487b9e..d2f3e91 100644 --- a/src/visual/shaders/crossShader.frag +++ b/src/visual/shaders/crossShader.frag @@ -16,11 +16,12 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uThickness; +uniform vec3 uColor; void main() { vec2 uv = vUvs; float sx = step(uThickness, length(uv.x * 2. - 1.)); float sy = step(uThickness, length(uv.y * 2. - 1.)); float s = 1. - sx * sy; - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/gaussShader.frag b/src/visual/shaders/gaussShader.frag index 3ba302c..efa69f1 100644 --- a/src/visual/shaders/gaussShader.frag +++ b/src/visual/shaders/gaussShader.frag @@ -18,6 +18,7 @@ out vec4 shaderOut; uniform float uA; uniform float uB; uniform float uC; +uniform vec3 uColor; #define M_PI 3.14159265358979 @@ -26,5 +27,5 @@ void main() { float c2 = uC * uC; float x = length(uv - .5); float g = uA * exp(-pow(x - uB, 2.) / c2 * .5); - shaderOut = vec4(vec3(g), 1.); + shaderOut = vec4(vec3(g) * uColor, 1.); } diff --git a/src/visual/shaders/radRampShader.frag b/src/visual/shaders/radRampShader.frag index 192acd4..af47d93 100644 --- a/src/visual/shaders/radRampShader.frag +++ b/src/visual/shaders/radRampShader.frag @@ -14,11 +14,12 @@ precision mediump float; in vec2 vUvs; out vec4 shaderOut; uniform float uSqueeze; +uniform vec3 uColor; #define M_PI 3.14159265358979 void main() { vec2 uv = vUvs; float s = 1. - length(uv * 2. - 1.) * uSqueeze; - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/raisedCosShader.frag b/src/visual/shaders/raisedCosShader.frag index 05e75cd..6d1eec2 100644 --- a/src/visual/shaders/raisedCosShader.frag +++ b/src/visual/shaders/raisedCosShader.frag @@ -18,6 +18,7 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uBeta; uniform float uPeriod; +uniform vec3 uColor; void main() { vec2 uv = vUvs; @@ -31,5 +32,5 @@ void main() { } else if (absX > edgeArgument2) { s = 0.; } - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/sawShader.frag b/src/visual/shaders/sawShader.frag index 0948bf7..829cbcc 100644 --- a/src/visual/shaders/sawShader.frag +++ b/src/visual/shaders/sawShader.frag @@ -18,10 +18,11 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uFreq; uniform float uPhase; +uniform vec3 uColor; void main() { vec2 uv = vUvs; float s = uFreq * uv.x + uPhase; s = mod(s, 1.); - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/sinShader.frag b/src/visual/shaders/sinShader.frag index 441d2fd..b55a402 100644 --- a/src/visual/shaders/sinShader.frag +++ b/src/visual/shaders/sinShader.frag @@ -22,6 +22,6 @@ uniform vec3 uColor; void main() { vec2 uv = vUvs; - float s = sin((uFreq * uv.x + uPhase) * 2. * M_PI); - shaderOut = vec4((.5 + .5 * vec3(s)) * uColor, 1.0); + float s = sin((uFreq * uv.x + uPhase) * 2. * M_PI) * .5 + .5; + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/sinXsinShader.frag b/src/visual/shaders/sinXsinShader.frag index 21d3975..88b4e0d 100644 --- a/src/visual/shaders/sinXsinShader.frag +++ b/src/visual/shaders/sinXsinShader.frag @@ -19,11 +19,12 @@ out vec4 shaderOut; #define PI2 2.* M_PI uniform float uFreq; uniform float uPhase; +uniform vec3 uColor; void main() { vec2 uv = vUvs; float sx = sin((uFreq * uv.x + uPhase) * PI2); float sy = sin((uFreq * uv.y + uPhase) * PI2); float s = sx * sy * .5 + .5; - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/sqrShader.frag b/src/visual/shaders/sqrShader.frag index f44ffca..2669c9b 100644 --- a/src/visual/shaders/sqrShader.frag +++ b/src/visual/shaders/sqrShader.frag @@ -18,9 +18,10 @@ out vec4 shaderOut; #define M_PI 3.14159265358979 uniform float uFreq; uniform float uPhase; +uniform vec3 uColor; void main() { vec2 uv = vUvs; - float s = sign(sin((uFreq * uv.x + uPhase) * 2. * M_PI)); - shaderOut = vec4(.5 + .5 * vec3(s), 1.0); + float s = sign(sin((uFreq * uv.x + uPhase) * 2. * M_PI)) * .5 + .5; + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/sqrXsqrShader.frag b/src/visual/shaders/sqrXsqrShader.frag index 6f140b4..f953f9c 100644 --- a/src/visual/shaders/sqrXsqrShader.frag +++ b/src/visual/shaders/sqrXsqrShader.frag @@ -19,11 +19,12 @@ out vec4 shaderOut; #define PI2 2.* M_PI uniform float uFreq; uniform float uPhase; +uniform vec3 uColor; void main() { vec2 uv = vUvs; float sx = sign(sin((uFreq * uv.x + uPhase) * PI2)); float sy = sign(sin((uFreq * uv.y + uPhase) * PI2)); float s = sx * sy * .5 + .5; - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); } diff --git a/src/visual/shaders/triShader.frag b/src/visual/shaders/triShader.frag index 445a0c4..0189a6c 100644 --- a/src/visual/shaders/triShader.frag +++ b/src/visual/shaders/triShader.frag @@ -19,10 +19,11 @@ out vec4 shaderOut; uniform float uFreq; uniform float uPhase; uniform float uPeriod; +uniform vec3 uColor; void main() { vec2 uv = vUvs; float s = uFreq * uv.x + uPhase; s = 2. * abs(s / uPeriod - floor(s / uPeriod + .5)); - shaderOut = vec4(vec3(s), 1.0); + shaderOut = vec4(vec3(s) * uColor, 1.0); }