From ff13338563c1e82f6b7c81bc5dfecec86fd8f758 Mon Sep 17 00:00:00 2001 From: lgtst Date: Wed, 2 Mar 2022 20:45:12 +0300 Subject: [PATCH] Full set of signals for grating stim; Documentation test; --- docs/visual_GratingStim.js.html | 644 ++++++++++++++++++++++++ src/visual/GratingStim.js | 193 +++++-- src/visual/shaders/circleShader.frag | 13 + src/visual/shaders/crossShader.frag | 16 + src/visual/shaders/gaussShader.frag | 17 +- src/visual/shaders/radRampShader.frag | 17 + src/visual/shaders/raisedCosShader.frag | 29 ++ src/visual/shaders/sawShader.frag | 21 + src/visual/shaders/sinXsinShader.frag | 17 + src/visual/shaders/sqrShader.frag | 20 + src/visual/shaders/sqrXsqrShader.frag | 17 + src/visual/shaders/triShader.frag | 22 + 12 files changed, 974 insertions(+), 52 deletions(-) create mode 100644 docs/visual_GratingStim.js.html create mode 100644 src/visual/shaders/circleShader.frag create mode 100644 src/visual/shaders/crossShader.frag create mode 100644 src/visual/shaders/radRampShader.frag create mode 100644 src/visual/shaders/raisedCosShader.frag create mode 100644 src/visual/shaders/sawShader.frag create mode 100644 src/visual/shaders/sinXsinShader.frag create mode 100644 src/visual/shaders/sqrShader.frag create mode 100644 src/visual/shaders/sqrXsqrShader.frag create mode 100644 src/visual/shaders/triShader.frag diff --git a/docs/visual_GratingStim.js.html b/docs/visual_GratingStim.js.html new file mode 100644 index 0000000..e6804ff --- /dev/null +++ b/docs/visual_GratingStim.js.html @@ -0,0 +1,644 @@ + + + + + JSDoc: Source: visual/GratingStim.js + + + + + + + + + + +
+ +

Source: visual/GratingStim.js

+ + + + + + +
+
+
/**
+ * Grating Stimulus.
+ *
+ * @author Alain Pitiot
+ * @version 2021.2.0
+ * @copyright (c) 2017-2020 Ilixa Ltd. (http://ilixa.com) (c) 2020-2021 Open Science Tools Ltd. (https://opensciencetools.org)
+ * @license Distributed under the terms of the MIT License
+ */
+
+import * as PIXI from "pixi.js-legacy";
+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";
+import defaultQuadVert from "./shaders/defaultQuad.vert";
+import sinShader from "./shaders/sinShader.frag";
+import sqrShader from "./shaders/sqrShader.frag";
+import sawShader from "./shaders/sawShader.frag";
+import triShader from "./shaders/triShader.frag";
+import sinXsinShader from "./shaders/sinXsinShader.frag";
+import sqrXsqrShader from "./shaders/sqrXsqrShader.frag";
+import circleShader from "./shaders/circleShader.frag";
+import gaussShader from "./shaders/gaussShader.frag";
+import crossShader from "./shaders/crossShader.frag";
+import radRampShader from "./shaders/radRampShader.frag";
+import raisedCosShader from "./shaders/raisedCosShader.frag";
+
+
+/**
+ * Grating Stimulus.
+ *
+ * @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
+ * @param {string | HTMLImageElement} options.image - the name of the image resource or the HTMLImageElement corresponding to the image
+ * @param {string | HTMLImageElement} options.mask - the name of the mask resource or HTMLImageElement corresponding to the mask
+ * @param {string} [options.units= "norm"] - the units of the stimulus (e.g. for size, position, vertices)
+ * @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 (the size of the image 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 {number} [options.depth= 0] - the depth (i.e. the z order)
+ * @param {number} [options.texRes= 128] - the resolution of the text
+ * @param {boolean} [options.interpolate= false] - whether or not the image is interpolated
+ * @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)
+{
+	// win,
+	// tex="sin",
+	// mask="none",
+	// units="",
+	// pos=(0.0, 0.0),
+	// size=None,
+	// sf=None,
+	// ori=0.0,
+	// phase=(0.0, 0.0),
+	// texRes=128,
+	// rgb=None,
+	// dkl=None,
+	// lms=None,
+	// color=(1.0, 1.0, 1.0),
+	// colorSpace='rgb',
+	// contrast=1.0,
+	// opacity=None,
+	// depth=0,
+	// rgbPedestal=(0.0, 0.0, 0.0),
+	// interpolate=False,
+	// blendmode='avg',
+	// name=None,
+	// autoLog=None,
+	// autoDraw=False,
+	// maskParams=None)
+
+	static #DEFINED_FUNCTIONS = {
+		sin: {
+			shader: sinShader,
+			uniforms: {
+				uFreq: 1.0,
+				uPhase: 0.0
+			}
+		},
+		sqr: {
+			shader: sqrShader,
+			uniforms: {
+				uFreq: 1.0,
+				uPhase: 0.0
+			}
+		},
+		saw: {
+			shader: sawShader,
+			uniforms: {
+				uFreq: 1.0,
+				uPhase: 0.0
+			}
+		},
+		tri: {
+			shader: triShader,
+			uniforms: {
+				uFreq: 1.0,
+				uPhase: 0.0,
+				uPeriod: 1.0
+			}
+		},
+		sinXsin: {
+			shader: sinXsinShader,
+			uniforms: {
+
+			}
+		},
+		sqrXsqr: {
+			shader: sqrXsqrShader,
+			uniforms: {
+				uFreq: 1.0,
+				uPhase: 0.0
+			}
+		},
+		circle: {
+			shader: circleShader,
+			uniforms: {
+
+			}
+		},
+		gauss: {
+			shader: gaussShader,
+			uniforms: {
+				uA: 1.0,
+				uB: 0.0,
+				uC: 0.16
+			}
+		},
+		cross: {
+			shader: crossShader,
+			uniforms: {
+				uThickness: 0.1
+			}
+		},
+		radRamp: {
+			shader: radRampShader,
+			uniforms: {
+
+			}
+		},
+		raisedCos: {
+			shader: raisedCosShader,
+			uniforms: {
+				uBeta: 0.25,
+				uPeriod: 1.0
+			}
+		}
+	};
+
+	static #DEFAULT_STIM_SIZE_PX = [256, 256]; // in pixels
+
+	constructor({
+		name,
+		tex = "sin",
+		win,
+		mask,
+		pos,
+		units,
+		spatialFrequency = 1.,
+		ori,
+		phase,
+		size,
+		rgb,
+	    dkl,
+	    lms,
+		color,
+		colorSpace,
+		opacity,
+		contrast,
+		texRes,
+		depth,
+		rgbPedestal,
+		interpolate,
+		blendmode,
+		autoDraw,
+		autoLog,
+		maskParams
+	} = {})
+	{
+		super({ name, win, units, ori, opacity, depth, pos, size, autoDraw, autoLog });
+
+		this._addAttribute(
+			"tex",
+			tex,
+		);
+		this._addAttribute(
+			"mask",
+			mask,
+		);
+		this._addAttribute(
+			"spatialFrequency",
+			spatialFrequency,
+			GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uFreq || 1.0
+		);
+		this._addAttribute(
+			"phase",
+			phase,
+			GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uPhase || 0.0
+		);
+		this._addAttribute(
+			"color",
+			color,
+			"white",
+			this._onChange(true, false),
+		);
+		this._addAttribute(
+			"contrast",
+			contrast,
+			1.0,
+			this._onChange(true, false),
+		);
+		this._addAttribute(
+			"texRes",
+			texRes,
+			128,
+			this._onChange(true, false),
+		);
+		this._addAttribute(
+			"interpolate",
+			interpolate,
+			false,
+			this._onChange(true, false),
+		);
+
+		// estimate the bounding box:
+		this._estimateBoundingBox();
+
+		if (this._autoLog)
+		{
+			this._psychoJS.experimentLogger.exp(`Created ${this.name} = ${this.toString()}`);
+		}
+
+		if (!Array.isArray(this.size) || this.size.length === 0) {
+			this.size = util.to_unit(GratingStim.#DEFAULT_STIM_SIZE_PX, "pix", this.win, this.units);
+		}
+		this._size_px = util.to_px(this.size, this.units, this.win);
+	}
+
+	/**
+	 * Setter for the image attribute.
+	 *
+	 * @name module:visual.GratingStim#setImage
+	 * @public
+	 * @param {HTMLImageElement | string} image - the name of the image resource or HTMLImageElement corresponding to the image
+	 * @param {boolean} [log= false] - whether of not to log
+	 */
+	setTex(tex, log = false)
+	{
+		const response = {
+			origin: "GratingStim.setTex",
+			context: "when setting the tex of GratingStim: " + this._name,
+		};
+
+		try
+		{
+			let hasChanged = false;
+
+			// tex is undefined: that's fine but we raise a warning in case this is a symptom of an actual problem
+			if (typeof tex === "undefined")
+			{
+				this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined.");
+				this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined");
+			}
+			else if (GratingStim.#DEFINED_FUNCTIONS[tex] !== undefined)
+			{
+				// tex is a string and it is one of predefined functions available in shaders
+				this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex);
+				const curFuncName = this.getTex();
+				hasChanged = curFuncName ? curFuncName !== tex : true;
+			}
+			else
+			{
+				// tex is a string: it should be the name of a resource, which we load
+				if (typeof tex === "string")
+				{
+					tex = this.psychoJS.serverManager.getResource(tex);
+				}
+
+				// tex should now be an actual HTMLImageElement: we raise an error if it is not
+				if (!(tex instanceof HTMLImageElement))
+				{
+					throw "the argument: " + tex.toString() + " is not an image\" }";
+				}
+
+				this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: src= " + tex.src + ", size= " + tex.width + "x" + tex.height);
+				const existingImage = this.getTex();
+				hasChanged = existingImage ? existingImage.src !== tex.src : true;
+			}
+
+			this._setAttribute("tex", tex, log);
+
+			if (hasChanged)
+			{
+				this._onChange(true, true)();
+			}
+		}
+		catch (error)
+		{
+			throw Object.assign(response, { error });
+		}
+	}
+
+	/**
+	 * Setter for the mask attribute.
+	 *
+	 * @name module:visual.GratingStim#setMask
+	 * @public
+	 * @param {HTMLImageElement | string} mask - the name of the mask resource or HTMLImageElement corresponding to the mask
+	 * @param {boolean} [log= false] - whether of not to log
+	 */
+	setMask(mask, log = false)
+	{
+		const response = {
+			origin: "GratingStim.setMask",
+			context: "when setting the mask of GratingStim: " + this._name,
+		};
+
+		try
+		{
+			// mask is undefined: that's fine but we raise a warning in case this is a sympton of an actual problem
+			if (typeof mask === "undefined")
+			{
+				this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined.");
+				this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined");
+			}
+			else if (GratingStim.#DEFINED_FUNCTIONS[mask] !== undefined)
+			{
+				// mask is a string and it is one of predefined functions available in shaders
+				this.psychoJS.logger.debug("the mask is one of predefined functions. Set the mask of GratingStim: " + this._name + " as: " + mask);
+			}
+			else
+			{
+				// mask is a string: it should be the name of a resource, which we load
+				if (typeof mask === "string")
+				{
+					mask = this.psychoJS.serverManager.getResource(mask);
+				}
+
+				// mask should now be an actual HTMLImageElement: we raise an error if it is not
+				if (!(mask instanceof HTMLImageElement))
+				{
+					throw "the argument: " + mask.toString() + " is not an image\" }";
+				}
+
+				this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height);
+			}
+
+			this._setAttribute("mask", mask, log);
+
+			this._onChange(true, false)();
+		}
+		catch (error)
+		{
+			throw Object.assign(response, { error });
+		}
+	}
+
+	/**
+	 * Get the size of the display image, which is either that of the GratingStim or that of the image
+	 * it contains.
+	 *
+	 * @name module:visual.GratingStim#_getDisplaySize
+	 * @private
+	 * @return {number[]} the size of the displayed image
+	 */
+	_getDisplaySize()
+	{
+		let displaySize = this.size;
+
+		if (typeof displaySize === "undefined")
+		{
+			// use the size of the pixi element, if we have access to it:
+			if (typeof this._pixi !== "undefined" && this._pixi.width > 0)
+			{
+				const pixiContainerSize = [this._pixi.width, this._pixi.height];
+				displaySize = util.to_unit(pixiContainerSize, "pix", this.win, this.units);
+			}
+		}
+
+		return displaySize;
+	}
+
+	/**
+	 * Estimate the bounding box.
+	 *
+	 * @name module:visual.GratingStim#_estimateBoundingBox
+	 * @function
+	 * @override
+	 * @protected
+	 */
+	_estimateBoundingBox()
+	{
+		const size = this._getDisplaySize();
+		if (typeof size !== "undefined")
+		{
+			this._boundingBox = new PIXI.Rectangle(
+				this._pos[0] - size[0] / 2,
+				this._pos[1] - size[1] / 2,
+				size[0],
+				size[1],
+			);
+		}
+
+		// TODO take the orientation into account
+	}
+
+	/**
+	 * Generate PIXI.Mesh object based on provided shader function name and uniforms.
+	 * 
+	 * @name module:visual.GratingStim#_getPixiMeshFromPredefinedShaders
+	 * @function
+	 * @private
+	 * @param {String} funcName - name of the shader function. Must be one of the DEFINED_FUNCTIONS
+	 * @param {Object} uniforms - a set of uniforms to supply to the shader. Mixed together with default uniform values.
+	 * @return {Pixi.Mesh} Pixi.Mesh object that represents shader and later added to the scene.
+	 */
+	_getPixiMeshFromPredefinedShaders (funcName = "", uniforms = {}) {
+		const geometry = new PIXI.Geometry();
+		geometry.addAttribute(
+			"aVertexPosition",
+			[
+				0, 0,
+				this._size_px[0], 0,
+				this._size_px[0], this._size_px[1],
+				0, this._size_px[1]
+			],
+			2
+		);
+		geometry.addAttribute(
+			"aUvs",
+			[0, 0, 1, 0, 1, 1, 0, 1],
+			2
+		);
+		geometry.addIndex([0, 1, 2, 0, 2, 3]);
+		const vertexSrc = defaultQuadVert;
+	    const fragmentSrc = GratingStim.#DEFINED_FUNCTIONS[funcName].shader;
+	    const uniformsFinal = Object.assign({}, GratingStim.#DEFINED_FUNCTIONS[funcName].uniforms, uniforms);
+		const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal);
+		return new PIXI.Mesh(geometry, shader);
+	}
+
+	/**
+	 * Set phase value for the function.
+	 * 
+	 * @name module:visual.GratingStim#setPhase
+	 * @public
+	 * @param {number} phase - phase value
+	 * @param {boolean} [log= false] - whether of not to log
+	 */ 
+	setPhase (phase, log = false) {
+		this._setAttribute("phase", phase, log);
+		if (this._pixi instanceof PIXI.Mesh) {
+			this._pixi.shader.uniforms.uPhase = phase;
+		} else if (this._pixi instanceof PIXI.TilingSprite) {
+			this._pixi.tilePosition.x = -phase * (this._size_px[0] * this._pixi.tileScale.x) / (2 * Math.PI)
+		}
+	}
+
+	/**
+	 * Set spatial frequency value for the function.
+	 * 
+	 * @name module:visual.GratingStim#setPhase
+	 * @public
+	 * @param {number} sf - spatial frequency value
+	 * @param {boolean} [log= false] - whether of not to log
+	 */ 
+	setSpatialFrequency (sf, log = false) {
+		this._setAttribute("spatialFrequency", sf, log);
+		if (this._pixi instanceof PIXI.Mesh) {
+			this._pixi.shader.uniforms.uFreq = sf;
+		} else if (this._pixi instanceof PIXI.TilingSprite) {
+			// tileScale units are pixels, so converting function frequency to pixels
+			// and also taking into account possible size difference between used texture and requested stim size
+			this._pixi.tileScale.x = (1 / sf) * (this._pixi.width / this._pixi.texture.width);
+		}
+	}
+
+	/**
+	 * Update the stimulus, if necessary.
+	 *
+	 * @name module:visual.GratingStim#_updateIfNeeded
+	 * @private
+	 */
+	_updateIfNeeded()
+	{
+		if (!this._needUpdate)
+		{
+			return;
+		}
+		this._needUpdate = false;
+
+		// update the PIXI representation, if need be:
+		if (this._needPixiUpdate)
+		{
+			this._needPixiUpdate = false;
+			if (typeof this._pixi !== "undefined")
+			{
+				this._pixi.destroy(true);
+			}
+			this._pixi = undefined;
+
+			// no image to draw: return immediately
+			if (typeof this._tex === "undefined")
+			{
+				return;
+			}
+
+			if (this._tex instanceof HTMLImageElement)
+			{
+				this._pixi = PIXI.TilingSprite.from(this._tex, {
+					width: this._size_px[0],
+					height: this._size_px[1]
+				});
+				this.setPhase(this._phase);
+				this.setSpatialFrequency(this._spatialFrequency);
+			}
+			else
+			{
+				this._pixi = this._getPixiMeshFromPredefinedShaders(this._tex, {
+					uFreq: this._spatialFrequency,
+					uPhase: this._phase
+				});
+			}
+			this._pixi.pivot.set(this._pixi.width * .5, this._pixi.width * .5);
+
+			// add a mask if need be:
+			if (typeof this._mask !== "undefined")
+			{
+				if (this._mask instanceof HTMLImageElement)
+				{
+					this._pixi.mask = PIXI.Sprite.from(this._mask);
+					this._pixi.addChild(this._pixi.mask);
+				}
+				else
+				{
+					// for some reason setting PIXI.Mesh as .mask doesn't do anything,
+					// rendering mask to texture for further use.
+					const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask);
+					const rt = PIXI.RenderTexture.create({
+						width: this._size_px[0],
+						height: this._size_px[1]
+					});
+					this.win._renderer.render(maskMesh, {
+						renderTexture: rt
+					});
+					const maskSprite = new PIXI.Sprite.from(rt);
+					this._pixi.mask = maskSprite;
+					this._pixi.addChild(maskSprite);
+				}
+			}
+
+			// since _pixi.width may not be immediately available but the rest of the code needs its value
+			// we arrange for repeated calls to _updateIfNeeded until we have a width:
+			if (this._pixi.width === 0)
+			{
+				this._needUpdate = true;
+				this._needPixiUpdate = true;
+				return;
+			}
+		}
+
+		this._pixi.zIndex = this._depth;
+		this._pixi.alpha = this.opacity;
+
+		// set the scale:
+		const displaySize = this._getDisplaySize();
+		this._size_px = util.to_px(displaySize, this.units, this.win);
+		const scaleX = this._size_px[0] / this._pixi.width;
+		const scaleY = this._size_px[1] / this._pixi.height;
+		this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX;
+		this._pixi.scale.y = this.flipVert ? scaleY : -scaleY;
+
+		// set the position, rotation, and anchor (image centered on pos):
+		let pos = to_pixiPoint(this.pos, this.units, this.win);
+		this._pixi.position.set(pos.x, pos.y);
+		this._pixi.rotation = this.ori * Math.PI / 180;
+
+		// re-estimate the bounding box, as the texture's width may now be available:
+		this._estimateBoundingBox();
+	}
+}
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/src/visual/GratingStim.js b/src/visual/GratingStim.js index b1f93ff..c06df13 100644 --- a/src/visual/GratingStim.js +++ b/src/visual/GratingStim.js @@ -13,25 +13,19 @@ 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"; -import defaultQuadVert from './shaders/defaultQuad.vert'; -import sinShader from './shaders/sinShader.frag'; -import gaussShader from './shaders/gaussShader.frag'; +import defaultQuadVert from "./shaders/defaultQuad.vert"; +import sinShader from "./shaders/sinShader.frag"; +import sqrShader from "./shaders/sqrShader.frag"; +import sawShader from "./shaders/sawShader.frag"; +import triShader from "./shaders/triShader.frag"; +import sinXsinShader from "./shaders/sinXsinShader.frag"; +import sqrXsqrShader from "./shaders/sqrXsqrShader.frag"; +import circleShader from "./shaders/circleShader.frag"; +import gaussShader from "./shaders/gaussShader.frag"; +import crossShader from "./shaders/crossShader.frag"; +import radRampShader from "./shaders/radRampShader.frag"; +import raisedCosShader from "./shaders/raisedCosShader.frag"; -const DEFINED_FUNCTIONS = { - sin: sinShader, - sqr: undefined, - saw: undefined, - tri: undefined, - sinXsin: undefined, - sqrXsqr: undefined, - circle: undefined, - gauss: gaussShader, - cross: undefined, - radRamp: undefined, - raisedCos: undefined -}; - -const DEFAULT_STIM_SIZE = [256, 256]; // in pixels /** * Grating Stimulus. @@ -49,7 +43,7 @@ const DEFAULT_STIM_SIZE = [256, 256]; // in pixels * @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 (the size of the image will be used if size is not specified) - * @param {Color} [options.color= 'white'] the background color + * @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 {number} [options.depth= 0] - the depth (i.e. the z order) @@ -86,14 +80,95 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) // autoLog=None, // autoDraw=False, // maskParams=None) + + static #DEFINED_FUNCTIONS = { + sin: { + shader: sinShader, + uniforms: { + uFreq: 1.0, + uPhase: 0.0 + } + }, + sqr: { + shader: sqrShader, + uniforms: { + uFreq: 1.0, + uPhase: 0.0 + } + }, + saw: { + shader: sawShader, + uniforms: { + uFreq: 1.0, + uPhase: 0.0 + } + }, + tri: { + shader: triShader, + uniforms: { + uFreq: 1.0, + uPhase: 0.0, + uPeriod: 1.0 + } + }, + sinXsin: { + shader: sinXsinShader, + uniforms: { + + } + }, + sqrXsqr: { + shader: sqrXsqrShader, + uniforms: { + uFreq: 1.0, + uPhase: 0.0 + } + }, + circle: { + shader: circleShader, + uniforms: { + + } + }, + gauss: { + shader: gaussShader, + uniforms: { + uA: 1.0, + uB: 0.0, + uC: 0.16 + } + }, + cross: { + shader: crossShader, + uniforms: { + uThickness: 0.1 + } + }, + radRamp: { + shader: radRampShader, + uniforms: { + + } + }, + raisedCos: { + shader: raisedCosShader, + uniforms: { + uBeta: 0.25, + uPeriod: 1.0 + } + } + }; + + static #DEFAULT_STIM_SIZE_PX = [256, 256]; // in pixels + constructor({ name, - tex, + tex = "sin", win, mask, pos, units, - spatialFrequency = 10., + spatialFrequency = 1., ori, phase, size, @@ -127,12 +202,12 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) this._addAttribute( "spatialFrequency", spatialFrequency, - 10. + GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uFreq || 1.0 ); this._addAttribute( "phase", phase, - 0. + GratingStim.#DEFINED_FUNCTIONS[tex].uniforms.uPhase || 0.0 ); this._addAttribute( "color", @@ -168,9 +243,9 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) } if (!Array.isArray(this.size) || this.size.length === 0) { - this.size = util.to_unit(DEFAULT_STIM_SIZE, "pix", this.win, this.units); + this.size = util.to_unit(GratingStim.#DEFAULT_STIM_SIZE_PX, "pix", this.win, this.units); } - this._sizeInPixels = util.to_px(this.size, this.units, this.win); + this._size_px = util.to_px(this.size, this.units, this.win); } /** @@ -198,7 +273,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) this.psychoJS.logger.warn("setting the tex of GratingStim: " + this._name + " with argument: undefined."); this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: undefined"); } - else if (DEFINED_FUNCTIONS[tex] !== undefined) + else if (GratingStim.#DEFINED_FUNCTIONS[tex] !== undefined) { // tex is a string and it is one of predefined functions available in shaders this.psychoJS.logger.debug("the tex is one of predefined functions. Set the tex of GratingStim: " + this._name + " as: " + tex); @@ -216,7 +291,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) // tex should now be an actual HTMLImageElement: we raise an error if it is not if (!(tex instanceof HTMLImageElement)) { - throw "the argument: " + tex.toString() + ' is not an image" }'; + throw "the argument: " + tex.toString() + " is not an image\" }"; } this.psychoJS.logger.debug("set the tex of GratingStim: " + this._name + " as: src= " + tex.src + ", size= " + tex.width + "x" + tex.height); @@ -260,7 +335,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) this.psychoJS.logger.warn("setting the mask of GratingStim: " + this._name + " with argument: undefined."); this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: undefined"); } - else if (DEFINED_FUNCTIONS[mask] !== undefined) + else if (GratingStim.#DEFINED_FUNCTIONS[mask] !== undefined) { // mask is a string and it is one of predefined functions available in shaders this.psychoJS.logger.debug("the mask is one of predefined functions. Set the mask of GratingStim: " + this._name + " as: " + mask); @@ -276,7 +351,7 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) // mask should now be an actual HTMLImageElement: we raise an error if it is not if (!(mask instanceof HTMLImageElement)) { - throw "the argument: " + mask.toString() + ' is not an image" }'; + throw "the argument: " + mask.toString() + " is not an image\" }"; } this.psychoJS.logger.debug("set the mask of GratingStim: " + this._name + " as: src= " + mask.src + ", size= " + mask.width + "x" + mask.height); @@ -341,42 +416,66 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) // TODO take the orientation into account } - _getPixiMeshFromPredefinedShaders (funcName = '', uniforms = {}) { + /** + * Generate PIXI.Mesh object based on provided shader function name and uniforms. + * + * @name module:visual.GratingStim#_getPixiMeshFromPredefinedShaders + * @function + * @private + * @param {String} funcName - name of the shader function. Must be one of the DEFINED_FUNCTIONS + * @param {Object} uniforms - a set of uniforms to supply to the shader. Mixed together with default uniform values. + * @return {Pixi.Mesh} Pixi.Mesh object that represents shader and later added to the scene. + */ + _getPixiMeshFromPredefinedShaders (funcName = "", uniforms = {}) { const geometry = new PIXI.Geometry(); geometry.addAttribute( - 'aVertexPosition', + "aVertexPosition", [ 0, 0, - this._sizeInPixels[0], 0, - this._sizeInPixels[0], this._sizeInPixels[1], - 0, this._sizeInPixels[1] + this._size_px[0], 0, + this._size_px[0], this._size_px[1], + 0, this._size_px[1] ], 2 ); geometry.addAttribute( - 'aUvs', + "aUvs", [0, 0, 1, 0, 1, 1, 0, 1], 2 ); geometry.addIndex([0, 1, 2, 0, 2, 3]); const vertexSrc = defaultQuadVert; - const fragmentSrc = DEFINED_FUNCTIONS[funcName]; - const uniformsFinal = Object.assign(uniforms, { - // for future default uniforms - }); + const fragmentSrc = GratingStim.#DEFINED_FUNCTIONS[funcName].shader; + const uniformsFinal = Object.assign({}, GratingStim.#DEFINED_FUNCTIONS[funcName].uniforms, uniforms); const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, uniformsFinal); return new PIXI.Mesh(geometry, shader); } + /** + * Set phase value for the function. + * + * @name module:visual.GratingStim#setPhase + * @public + * @param {number} phase - phase value + * @param {boolean} [log= false] - whether of not to log + */ setPhase (phase, log = false) { this._setAttribute("phase", phase, log); if (this._pixi instanceof PIXI.Mesh) { this._pixi.shader.uniforms.uPhase = phase; } else if (this._pixi instanceof PIXI.TilingSprite) { - this._pixi.tilePosition.x = -phase * (this._sizeInPixels[0] * this._pixi.tileScale.x) / (2 * Math.PI) + this._pixi.tilePosition.x = -phase * (this._size_px[0] * this._pixi.tileScale.x) / (2 * Math.PI) } } + /** + * Set spatial frequency value for the function. + * + * @name module:visual.GratingStim#setPhase + * @public + * @param {number} sf - spatial frequency value + * @param {boolean} [log= false] - whether of not to log + */ setSpatialFrequency (sf, log = false) { this._setAttribute("spatialFrequency", sf, log); if (this._pixi instanceof PIXI.Mesh) { @@ -421,8 +520,8 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) if (this._tex instanceof HTMLImageElement) { this._pixi = PIXI.TilingSprite.from(this._tex, { - width: this._sizeInPixels[0], - height: this._sizeInPixels[1] + width: this._size_px[0], + height: this._size_px[1] }); this.setPhase(this._phase); this.setSpatialFrequency(this._spatialFrequency); @@ -450,8 +549,8 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) // rendering mask to texture for further use. const maskMesh = this._getPixiMeshFromPredefinedShaders(this._mask); const rt = PIXI.RenderTexture.create({ - width: this._sizeInPixels[0], - height: this._sizeInPixels[1] + width: this._size_px[0], + height: this._size_px[1] }); this.win._renderer.render(maskMesh, { renderTexture: rt @@ -477,9 +576,9 @@ export class GratingStim extends util.mix(VisualStim).with(ColorMixin) // set the scale: const displaySize = this._getDisplaySize(); - this._sizeInPixels = util.to_px(displaySize, this.units, this.win); - const scaleX = this._sizeInPixels[0] / this._pixi.width; - const scaleY = this._sizeInPixels[1] / this._pixi.height; + this._size_px = util.to_px(displaySize, this.units, this.win); + const scaleX = this._size_px[0] / this._pixi.width; + const scaleY = this._size_px[1] / this._pixi.height; this._pixi.scale.x = this.flipHoriz ? -scaleX : scaleX; this._pixi.scale.y = this.flipVert ? scaleY : -scaleY; diff --git a/src/visual/shaders/circleShader.frag b/src/visual/shaders/circleShader.frag new file mode 100644 index 0000000..3211825 --- /dev/null +++ b/src/visual/shaders/circleShader.frag @@ -0,0 +1,13 @@ +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 + +void main() { + vec2 uv = vUvs; + float s = 1. - step(.5, length(uv - .5)); + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/crossShader.frag b/src/visual/shaders/crossShader.frag new file mode 100644 index 0000000..dbff4c1 --- /dev/null +++ b/src/visual/shaders/crossShader.frag @@ -0,0 +1,16 @@ +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uThickness; + +void main() { + vec2 uv = vUvs; + float sx = step(uThickness, length(uv.x - .5)); + float sy = step(uThickness, length(uv.y - .5)); + float s = 1. - sx * sy; + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/gaussShader.frag b/src/visual/shaders/gaussShader.frag index 17543f6..ed7fbec 100644 --- a/src/visual/shaders/gaussShader.frag +++ b/src/visual/shaders/gaussShader.frag @@ -1,17 +1,24 @@ +// +// Gaussian Function: +// https://en.wikipedia.org/wiki/Gaussian_function +// + #version 300 es precision mediump float; in vec2 vUvs; out vec4 shaderOut; -#define M_PI 3.14159265358979 +uniform float uA; +uniform float uB; +uniform float uC; -float gauss(float x) { - return exp(-(x * x) * 20.); -} +#define M_PI 3.14159265358979 void main() { vec2 uv = vUvs; - float g = gauss(uv.x - .5) * gauss(uv.y - .5); + float c2 = uC * uC; + float x = length(uv - .5); + float g = uA * exp(-pow(x - uB, 2.) / c2 * .5); shaderOut = vec4(vec3(g), 1.); } diff --git a/src/visual/shaders/radRampShader.frag b/src/visual/shaders/radRampShader.frag new file mode 100644 index 0000000..bbbc586 --- /dev/null +++ b/src/visual/shaders/radRampShader.frag @@ -0,0 +1,17 @@ +// +// Radial ramp function +// + +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 + +void main() { + vec2 uv = vUvs; + float s = 1. - length(uv * 2. - 1.); + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/raisedCosShader.frag b/src/visual/shaders/raisedCosShader.frag new file mode 100644 index 0000000..605fbfe --- /dev/null +++ b/src/visual/shaders/raisedCosShader.frag @@ -0,0 +1,29 @@ +// +// Raised-cosine function: +// https://en.wikipedia.org/wiki/Raised-cosine_filter +// + +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uBeta; +uniform float uPeriod; + +void main() { + vec2 uv = vUvs; + float absX = length(uv * 2. - 1.); + float edgeArgument1 = (1. - uBeta) / (2. * uPeriod); + float edgeArgument2 = (1. + uBeta) / (2. * uPeriod); + float frequencyFactor = (M_PI * uPeriod) / uBeta; + float s = .5 * (1. + cos(frequencyFactor * (absX - edgeArgument1))); + if (absX <= edgeArgument1) { + s = 1.; + } else if (absX > edgeArgument2) { + s = 0.; + } + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/sawShader.frag b/src/visual/shaders/sawShader.frag new file mode 100644 index 0000000..ed55ceb --- /dev/null +++ b/src/visual/shaders/sawShader.frag @@ -0,0 +1,21 @@ +// +// Sawtooth wave: +// https://en.wikipedia.org/wiki/Sawtooth_wave +// + +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; + +void main() { + vec2 uv = vUvs; + float s = uFreq * uv.x + uPhase; + s = mod(s, 1.); + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/sinXsinShader.frag b/src/visual/shaders/sinXsinShader.frag new file mode 100644 index 0000000..15435ac --- /dev/null +++ b/src/visual/shaders/sinXsinShader.frag @@ -0,0 +1,17 @@ +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; + +void main() { + vec2 uv = vUvs; + float sx = sin(uFreq * uv.x * 2. * M_PI + uPhase); + float sy = sin(uFreq * uv.y * 2. * M_PI + uPhase); + float s = sx * sy * .5 + .5; + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/sqrShader.frag b/src/visual/shaders/sqrShader.frag new file mode 100644 index 0000000..dc9bd74 --- /dev/null +++ b/src/visual/shaders/sqrShader.frag @@ -0,0 +1,20 @@ +// +// Square wave: +// https://en.wikipedia.org/wiki/Square_wave +// + +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; + +void main() { + vec2 uv = vUvs; + float s = sign(sin(uFreq * uv.x * 2. * M_PI + uPhase)); + shaderOut = vec4(.5 + .5 * vec3(s), 1.0); +} diff --git a/src/visual/shaders/sqrXsqrShader.frag b/src/visual/shaders/sqrXsqrShader.frag new file mode 100644 index 0000000..4eb6cfa --- /dev/null +++ b/src/visual/shaders/sqrXsqrShader.frag @@ -0,0 +1,17 @@ +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; + +void main() { + vec2 uv = vUvs; + float sx = sign(sin(uFreq * uv.x * 2. * M_PI + uPhase)); + float sy = sign(sin(uFreq * uv.y * 2. * M_PI + uPhase)); + float s = sx * sy * .5 + .5; + shaderOut = vec4(vec3(s), 1.0); +} diff --git a/src/visual/shaders/triShader.frag b/src/visual/shaders/triShader.frag new file mode 100644 index 0000000..5b45ce0 --- /dev/null +++ b/src/visual/shaders/triShader.frag @@ -0,0 +1,22 @@ +// +// Triangle wave: +// https://en.wikipedia.org/wiki/Triangle_wave +// + +#version 300 es +precision mediump float; + +in vec2 vUvs; +out vec4 shaderOut; + +#define M_PI 3.14159265358979 +uniform float uFreq; +uniform float uPhase; +uniform float uPeriod; + +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); +}