1
0
mirror of https://github.com/psychopy/psychojs.git synced 2025-05-10 10:40:54 +00:00

added per grating and per window contrast filter; added separate stimuli PIXI container as a result of workaround with background; added grating blend mode support; started work on grating colors;

This commit is contained in:
lgtst 2022-05-05 19:37:30 +03:00
parent 496154442f
commit a1effe6969
15 changed files with 187 additions and 72 deletions

View File

@ -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;

View File

@ -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];

View File

@ -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,
);

View File

@ -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.<number>} [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")

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}