├── assets ├── whitedot.png ├── scale │ ├── arousal.png │ ├── valence.png │ ├── arousal.jpeg │ ├── valence.jpeg │ ├── arousal_ori.png │ └── valence_ori.png ├── stimuli │ └── scale │ │ ├── arousal │ │ ├── arousal0.png │ │ ├── arousal1.png │ │ ├── arousal2.png │ │ ├── arousal3.png │ │ ├── arousal4.png │ │ ├── arousal5.png │ │ ├── arousal6.png │ │ ├── arousal7.png │ │ └── arousal8.png │ │ └── valence │ │ ├── valence0.png │ │ ├── valence1.png │ │ ├── valence2.png │ │ ├── valence3.png │ │ ├── valence4.png │ │ ├── valence5.png │ │ ├── valence6.png │ │ ├── valence7.png │ │ └── valence8.png └── plugins │ └── jspsych-html-double-slider-response.js ├── webpack.config.js ├── index.html ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ └── user-story.md ├── .gitignore ├── src ├── utils │ ├── fileUtils.js │ ├── randomUtils.js │ └── imageUtils.js ├── foils.js ├── param.example.js ├── exemplars.js └── experiment.js ├── README.md ├── package.json ├── predictiveaffect.css ├── test ├── exemplars.test.js └── foils.test.js └── LICENSE /assets/whitedot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/whitedot.png -------------------------------------------------------------------------------- /assets/scale/arousal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/scale/arousal.png -------------------------------------------------------------------------------- /assets/scale/valence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/scale/valence.png -------------------------------------------------------------------------------- /assets/scale/arousal.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/scale/arousal.jpeg -------------------------------------------------------------------------------- /assets/scale/valence.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/scale/valence.jpeg -------------------------------------------------------------------------------- /assets/scale/arousal_ori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/scale/arousal_ori.png -------------------------------------------------------------------------------- /assets/scale/valence_ori.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/scale/valence_ori.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal0.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal1.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal2.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal3.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal4.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal5.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal6.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal7.png -------------------------------------------------------------------------------- /assets/stimuli/scale/arousal/arousal8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/arousal/arousal8.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence0.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence1.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence2.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence3.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence4.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence5.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence6.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence7.png -------------------------------------------------------------------------------- /assets/stimuli/scale/valence/valence8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandboxnu/predictive-affect/master/assets/stimuli/scale/valence/valence8.png -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './src/experiment.js', 5 | output: { 6 | filename: 'experiment.js', 7 | path: path.resolve(__dirname, 'dist') 8 | }, 9 | externals: { 10 | param: "param", 11 | }, 12 | devtool: 'cheap-module-source-map' 13 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |Hello world
"); 7 | global.window = window; 8 | global.document = window.document; 9 | 10 | const { populateExemplars, Exemplar } = require("../src/exemplars"); 11 | const paramSimple = { 12 | exemplarTypes: ['NNN', 'BBB', 'NNB', 'NBB'], 13 | numExemplarsPerType: 1 14 | } 15 | 16 | const paramHalfRand = { 17 | exemplarTypes: ['NNN', 'rand_BBB', 'NNB', 'rand_NBB'], 18 | numExemplarsPerType: 1 19 | } 20 | 21 | const exemplarsFromSimple = { 22 | NNN1: new Exemplar('NNN'), 23 | BBB1: new Exemplar('BBB'), 24 | NNB1: new Exemplar('NNB'), 25 | NBB1: new Exemplar('NBB'), 26 | } 27 | 28 | const halfRandExemplars = { 29 | NNN1: new Exemplar('NNN'), 30 | BBB1: new Exemplar('rand_BBB'), 31 | NNB1: new Exemplar('NNB'), 32 | NBB1: new Exemplar('rand_NBB'), 33 | } 34 | 35 | const paramLotsPerType = { 36 | exemplarTypes: ['NNN', 'BBB'], 37 | numExemplarsPerType: 13, 38 | } 39 | 40 | const paramTiny = { 41 | exemplarTypes: ['NNN'], 42 | numExemplarsPerType: 1, 43 | } 44 | 45 | describe("Exemplars from simple param", () => { 46 | let exemplars; 47 | before(() => { 48 | exemplars = populateExemplars(paramSimple); 49 | }); 50 | 51 | it("generates the correct number of exemplars", () => { 52 | assert.equal( 53 | Object.keys(exemplars).length, 54 | paramSimple.exemplarTypes.length * paramSimple.numExemplarsPerType 55 | ); 56 | }); 57 | it("generates the correct exemplar types", () => { 58 | assert.deepEqual( 59 | Object.keys(exemplars), 60 | Object.keys(exemplarsFromSimple) 61 | ); 62 | }); 63 | it("generates exemplars with information", () => { 64 | assert.ok(exemplars.BBB1.getImages().length === 3); 65 | assert.ok(exemplars.NNN1.getImages().length === 3); 66 | assert.ok(exemplars.NNB1.getImages().length === 3); 67 | assert.ok(exemplars.NBB1.getImages().length === 3); 68 | assert.ok(exemplars.BBB1.type === 'BBB'); 69 | assert.ok(exemplars.NNN1.type === 'NNN'); 70 | assert.ok(exemplars.NNB1.type === 'NNB'); 71 | assert.ok(exemplars.NBB1.type === 'NBB'); 72 | }); 73 | }); 74 | 75 | describe("Exemplars from half random param", () => { 76 | let exemplars; 77 | before(() => { 78 | exemplars = populateExemplars(paramHalfRand); 79 | }); 80 | 81 | it("generates the correct number of exemplars", () => { 82 | assert.equal( 83 | Object.keys(exemplars).length, 84 | paramHalfRand.exemplarTypes.length * paramHalfRand.numExemplarsPerType 85 | ); 86 | }); 87 | it("generates the correct exemplar types", () => { 88 | assert.deepEqual( 89 | Object.keys(exemplars), 90 | Object.keys(halfRandExemplars) 91 | ); 92 | }); 93 | it("generates exemplars with information", () => { 94 | assert.ok(exemplars.BBB1.getImages().length === 3); 95 | assert.ok(exemplars.NNN1.getImages().length === 3); 96 | assert.ok(exemplars.NNB1.getImages().length === 3); 97 | assert.ok(exemplars.NBB1.getImages().length === 3); 98 | assert.ok(exemplars.BBB1.type === 'BBB'); 99 | assert.ok(exemplars.NNN1.type === 'NNN'); 100 | assert.ok(exemplars.NNB1.type === 'NNB'); 101 | assert.ok(exemplars.NBB1.type === 'NBB'); 102 | assert.ok(exemplars.NNN1.isRand === false); 103 | assert.ok(exemplars.NNB1.isRand === false); 104 | assert.ok(exemplars.BBB1.isRand === true); 105 | assert.ok(exemplars.NBB1.isRand === true); 106 | }); 107 | }); 108 | 109 | describe('Exemplars from many-per-type param', () => { 110 | let exemplars; 111 | before(() => { 112 | exemplars = populateExemplars(paramLotsPerType); 113 | }); 114 | it("generates the correct amount of exemplars", () => { 115 | assert.equal(Object.keys(exemplars).length, paramLotsPerType.numExemplarsPerType * 2); 116 | }); 117 | it("generates the correct exemplar types", () => { 118 | for (let i = 1; i < paramLotsPerType.numExemplarsPerType; i += 1) { 119 | assert(Object.keys(exemplars).includes(`NNN${i}`)) 120 | } 121 | for (let i = 1; i < paramLotsPerType.numExemplarsPerType; i += 1) { 122 | assert(Object.keys(exemplars).includes(`BBB${i}`)) 123 | } 124 | }); 125 | it("generates exemplars with information", () => { 126 | for (let exemplar of Object.values(exemplars)) { 127 | assert.equal(exemplar.getImages().length, 3); 128 | } 129 | }); 130 | }); 131 | 132 | describe('Exemplar from tiny param', () => { 133 | let exemplars; 134 | before(() => { 135 | exemplars = populateExemplars(paramTiny); 136 | }); 137 | it("generates only one exemplar", () => { 138 | assert.equal(Object.keys(exemplars).length, 1); 139 | }); 140 | it("generates an exemplar with exactly three images", () => { 141 | assert.equal(exemplars['NNN1'].getImages().length, 3) 142 | }) 143 | }) -------------------------------------------------------------------------------- /src/exemplars.js: -------------------------------------------------------------------------------- 1 | const param = require("param"); 2 | const { copyImage } = require("./utils/imageUtils"); 3 | const { 4 | randomlyPickFromList, 5 | pickRandomBetween, 6 | randomlySelectImage 7 | } = require("./utils/randomUtils"); 8 | 9 | /** 10 | * Exemplar is a set of n images. 11 | */ 12 | class Exemplar { 13 | constructor(type) { 14 | this.images = []; 15 | this.isRand = type.startsWith("rand_"); 16 | this.type = type.replace("rand_", ""); 17 | this.populateImages(); 18 | } 19 | 20 | copy() { 21 | if (this == null || typeof this !== "object") return this; 22 | const copy = new Exemplar(this.type); 23 | copy.images = []; 24 | copy.isRand = this.isRand; 25 | this.getImages().forEach(image => { 26 | copy.images.push(copyImage(image)); 27 | }); 28 | return copy; 29 | } 30 | 31 | changeImageAt(index, img) { 32 | this.images[index] = img; 33 | } 34 | 35 | /** 36 | * Gets the image at index i 37 | * @param {*} index which image to select 38 | * @returns the Image object 39 | */ 40 | getImage(index) { 41 | return this.images[index]; 42 | } 43 | 44 | getImages() { 45 | return (param.randomTriplets || this.isRand) 46 | ? this.images.slice().sort( () => Math.random() - 0.5) 47 | : [...this.images]; 48 | } 49 | 50 | getImageNames() { 51 | return this.images.map(x => x.fileName); 52 | } 53 | 54 | populateImages() { 55 | const { type, images } = this; 56 | for (let charIndex = 0; charIndex < type.length; charIndex += 1) { 57 | const imgValence = type.charAt(charIndex); 58 | const dotSide = randomlyPickFromList(dotSides); 59 | const image = { 60 | fileName: randomlySelectImage(imgValence, currentList), 61 | valence: imgValence, 62 | dotPlacement: dotSide, 63 | greyDotX: 64 | dotSide === "left" 65 | ? pickRandomBetween( 66 | param.grey_radius, 67 | param.img_x / 2 - param.grey_radius 68 | ) 69 | : pickRandomBetween( 70 | param.img_x / 2 + param.grey_radius, 71 | param.img_x - param.grey_radius 72 | ), 73 | greyDotY: pickRandomBetween( 74 | param.grey_radius, 75 | param.img_y - param.grey_radius 76 | ), 77 | positionInExemplar: charIndex 78 | }; 79 | images.push(image); 80 | } 81 | } 82 | } 83 | 84 | /* EXEMPLARS */ 85 | 86 | // current list of used stimuli 87 | let currentList = []; 88 | 89 | // current sides of grey dots, for counterbalancing 90 | const dotSides = []; 91 | // trials per block / repeats of each image per block = # images per block 92 | for (let i = 0; i < param.trialsPerEncodingBlock / param.repPerBlock; i += 1) { 93 | // add an even amount of 'left'/'right' to the list, equal to the amount of images 94 | dotSides.push(i % 2 === 0 ? "left" : "right"); 95 | } 96 | 97 | /* 98 | Provided variables: 99 | exemplars: 100 | - object with string keys 101 | - keys are "NNN", "BBB", etc to match each unique type in param.exemplarTypes 102 | - values are arrays of each exemplar per type 103 | exemplars 104 | - object with string keys 105 | - keys are "NNN1", "NNN2", "NBB1", "NBB2", "NBB3", etc to match param.exemplarTypes 106 | - values are exemplar objects 107 | */ 108 | 109 | const populateExemplars = (param = {}, exemplars = {}) => { 110 | if (currentList.length > 0) { 111 | /* CONSTRAINT: can only have one valid set of exemplars at a time */ 112 | currentList = [] 113 | } 114 | const denormalizedExemplars = {}; 115 | 116 | const totalExemplarsCount = param.exemplarTypes.length * param.numExemplarsPerType; 117 | for (let i = 0; i < totalExemplarsCount; i += 1) { 118 | const type = param.exemplarTypes[i % param.exemplarTypes.length]; 119 | const nonRandomType = type.replace("rand_", "") 120 | if (!Array.isArray(denormalizedExemplars[nonRandomType])) 121 | denormalizedExemplars[nonRandomType] = []; 122 | denormalizedExemplars[nonRandomType].push(new Exemplar(type)); 123 | } 124 | Object.keys(denormalizedExemplars).forEach(type => { 125 | for (let i = 1; i <= denormalizedExemplars[type].length; i += 1) { 126 | exemplars[`${type}${i}`] = denormalizedExemplars[type][i - 1]; 127 | } 128 | }); 129 | 130 | return exemplars; 131 | }; 132 | 133 | const exemplars = populateExemplars(param); 134 | 135 | const createExemplarCounts = curExemplars => { 136 | const exemplarCounts = {}; 137 | Object.keys(curExemplars).forEach(name => { 138 | exemplarCounts[name] = param.repPerBlock; 139 | }); 140 | return exemplarCounts; 141 | }; 142 | 143 | // takes in exemplars and returns data nicely formatted for csv 144 | const normalizeExemplars = exmps => { 145 | const data = []; 146 | Object.entries(exmps).forEach(exemplarEntry => { 147 | const exemplar = exemplarEntry[1]; 148 | exemplar.getImages().forEach(image => { 149 | data.push({ 150 | exemplarName: exemplarEntry[0], 151 | type: exemplar.type, 152 | ...image 153 | }); 154 | }); 155 | }); 156 | return data; 157 | }; 158 | 159 | module.exports = { 160 | Exemplar, 161 | normalizeExemplars, 162 | createExemplarCounts, 163 | populateExemplars, 164 | exemplars 165 | }; 166 | -------------------------------------------------------------------------------- /test/foils.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it, before } = global; 2 | const assert = require("assert"); 3 | const jsdom = require("jsdom"); 4 | /* this weird stuff basically makes jsPsych testable in Node */ 5 | const { JSDOM } = jsdom; 6 | const { window } = new JSDOM("Hello world
"); 7 | global.window = window; 8 | global.document = window.document; 9 | 10 | const { populateExemplars} = require("../src/exemplars"); 11 | const { createFoils} = require("../src/foils"); 12 | 13 | const paramSimple = { 14 | exemplarTypes: ['NNN', 'BBB', 'NNB', 'NBB'], 15 | numExemplarsPerType: 1, 16 | foilTestedOn: [1, 0, 0, 1], 17 | foilTestedType: [false, true, false, true] 18 | } 19 | 20 | const paramHalfRand = { 21 | exemplarTypes: ['NNN', 'rand_BBB', 'NNB', 'rand_NBB'], 22 | numExemplarsPerType: 1, 23 | foilTestedOn: [1, 0, 0, 1], 24 | foilTestedType: [false, true, false, true] 25 | } 26 | 27 | const paramLotsPerType = { 28 | exemplarTypes: ['NNN', 'BBB'], 29 | foilTestedOn: [1, 0, 1, 0, 0, 0, 0, 1, 2, 2, 0, 1, 2, 1, 0, 1, 0, 0, 0, 0], 30 | numExemplarsPerType: 10, 31 | foilTestedType: [false, false, false, true, true, true, false, true, false, true, true, true, false, true, false, false, false, true, false, true] 32 | } 33 | 34 | const paramTiny = { 35 | exemplarTypes: ['NNN'], 36 | foilTestedOn: [1], 37 | numExemplarsPerType: 1, 38 | foilTestedType: [false] 39 | } 40 | 41 | describe("Foil generation from simple params", () => { 42 | let exemplars, foils, randExemplars, randFoils; 43 | before(() => { 44 | exemplars = populateExemplars(paramSimple); 45 | foils = createFoils(exemplars, paramSimple); 46 | randExemplars = populateExemplars(paramHalfRand); 47 | randFoils = createFoils(randExemplars, paramHalfRand); 48 | }); 49 | 50 | it("creats the correct number of foils", () => { 51 | assert.equal( 52 | Object.keys(exemplars).length, 53 | Object.keys(foils).length 54 | ); 55 | }); 56 | 57 | it("creates foils that are not the same as exemplars", () => { 58 | assert.notDeepEqual( 59 | exemplars, 60 | foils 61 | ); 62 | }); 63 | it("creates unique foils", () => { 64 | for (let type of Object.keys(exemplars)) { 65 | exemplar = exemplars[type]; 66 | foil = foils[type]; 67 | assert.notDeepEqual(foil.getImages(), exemplar.getImages()); 68 | } 69 | }); 70 | it("creates foils that are only different by one image", () => { 71 | for (let type of Object.keys(exemplars)) { 72 | exemplarImages = exemplars[type].getImageNames(); 73 | foilImages = foils[type].getImageNames(); 74 | sameImages = exemplarImages.filter(image => foilImages.includes(image)); 75 | assert.equal(sameImages.length, 2); 76 | } 77 | }); 78 | it("creates foils of the proper length", () => { 79 | for (let foil of Object.values(foils)) { 80 | assert.equal(foil.getImages().length, 3); 81 | } 82 | }); 83 | it("creates foils with the correct information in relation to the exemplars and params", () => { 84 | assert.ok(randFoils.BBB1.type === 'NBB'); 85 | assert.ok(randFoils.NNN1.type === 'NNN'); 86 | assert.ok(randFoils.NNB1.type === 'NNB'); 87 | assert.ok(randFoils.NBB1.type === 'NNB'); 88 | assert.ok(randExemplars.BBB1.type === 'BBB'); 89 | assert.ok(randExemplars.NNN1.type === 'NNN'); 90 | assert.ok(randExemplars.NNB1.type === 'NNB'); 91 | assert.ok(randExemplars.NBB1.type === 'NBB'); 92 | }); 93 | it("preserves randomness", () => { 94 | assert.ok(randFoils.NNN1.isRand === false); 95 | assert.ok(randFoils.NNB1.isRand === false); 96 | assert.ok(randFoils.BBB1.isRand === true); 97 | assert.ok(randFoils.NBB1.isRand === true); 98 | assert.ok(randExemplars.NNN1.isRand === false); 99 | assert.ok(randExemplars.NNB1.isRand === false); 100 | assert.ok(randExemplars.BBB1.isRand === true); 101 | assert.ok(randExemplars.NBB1.isRand === true); 102 | }); 103 | }); 104 | describe("Foil generation from many per type param", () => { 105 | let manyExemplars, manyFoils; 106 | before(() => { 107 | manyExemplars = populateExemplars(paramLotsPerType); 108 | manyFoils = createFoils(manyExemplars, paramLotsPerType); 109 | }); 110 | it("creats the correct number of foils", () => { 111 | assert.equal( 112 | Object.keys(manyExemplars).length, 113 | Object.keys(manyFoils).length 114 | ); 115 | }); 116 | it("creates foils that are only different by one image", () => { 117 | for (let type of Object.keys(manyExemplars)) { 118 | exemplarImages = manyExemplars[type].getImageNames(); 119 | foilImages = manyFoils[type].getImageNames(); 120 | sameImages = exemplarImages.filter(image => foilImages.includes(image)); 121 | assert.equal(sameImages.length, 2); 122 | } 123 | }); 124 | }); 125 | describe("Foil generation from tiny param", () => { 126 | let tinyExemplars, tinyFoils; 127 | before(() => { 128 | tinyExemplars = populateExemplars(paramTiny); 129 | tinyFoils = createFoils(tinyExemplars, paramTiny); 130 | }); 131 | it("creats the correct number of foils", () => { 132 | assert.equal( 133 | Object.keys(tinyExemplars).length, 134 | Object.keys(tinyFoils).length 135 | ); 136 | }); 137 | it("creates foils that are only different by one image", () => { 138 | for (let type of Object.keys(tinyExemplars)) { 139 | exemplarImages = tinyExemplars[type].getImageNames(); 140 | foilImages = tinyFoils[type].getImageNames(); 141 | sameImages = exemplarImages.filter(image => foilImages.includes(image)); 142 | assert.equal(sameImages.length, 2); 143 | } 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /src/utils/imageUtils.js: -------------------------------------------------------------------------------- 1 | const jsPsych = require("jspsych"); 2 | const param = require("param"); 3 | 4 | // Utilities for use in the html scripts 5 | const neuImages = [ 6 | "1908.jpg", 7 | "2101.jpg", 8 | "2191.jpg", 9 | "1945.jpg", 10 | "1390.jpg", 11 | "2520.jpg", 12 | "2107.jpg", 13 | "2393.jpg", 14 | "2484.jpg", 15 | "2597.jpg", 16 | "2020.jpg", 17 | "1903.jpg", 18 | "2309.jpg", 19 | "1645.jpg", 20 | "1114.jpg", 21 | "2635.jpg", 22 | "2272.jpg", 23 | "1302.jpg", 24 | "1122.jpg", 25 | "2383.jpg", 26 | "2359.jpg", 27 | "2840.jpg", 28 | "2575.jpg", 29 | "2122.jpg", 30 | "2890.jpg", 31 | "2220.jpg", 32 | "2411.jpg", 33 | "1617.jpg", 34 | "1670.jpg", 35 | "2384.jpg", 36 | "2749.jpg", 37 | "1935.jpg", 38 | "2279.jpg", 39 | "2397.jpg", 40 | "2210.jpg", 41 | "2377.jpg", 42 | "2579.jpg", 43 | "2458.jpg", 44 | "2445.jpg", 45 | "2308.jpg", 46 | "2446.jpg", 47 | "1560.jpg", 48 | "2032.jpg", 49 | "2206.jpg", 50 | "2221.jpg", 51 | "2752.jpg", 52 | "1947.jpg", 53 | "1931.jpg", 54 | "2435.jpg", 55 | "2102.jpg", 56 | "2235.jpg", 57 | "2396.jpg", 58 | "1230.jpg", 59 | "2215.jpg", 60 | "2695.jpg", 61 | "2745.1.jpg", 62 | "2521.jpg", 63 | "2870.jpg", 64 | "1726.jpg", 65 | "1350.jpg", 66 | "2704.jpg", 67 | "1820.jpg", 68 | "1675.jpg", 69 | "2606.jpg", 70 | "1616.jpg", 71 | "2770.jpg", 72 | "2850.jpg" 73 | ]; 74 | const negImages = [ 75 | "9909.jpg", 76 | "7380.jpg", 77 | "9530.jpg", 78 | "3261.jpg", 79 | "6315.jpg", 80 | "9571.jpg", 81 | "9412.jpg", 82 | "2456.jpg", 83 | "9220.jpg", 84 | "3019.jpg", 85 | "2352.2.jpg", 86 | "9187.jpg", 87 | "2811.jpg", 88 | "9006.jpg", 89 | "9183.jpg", 90 | "9927.jpg", 91 | "2688.jpg", 92 | "9414.jpg", 93 | "3350.jpg", 94 | "3212.jpg", 95 | "3001.jpg", 96 | "9280.jpg", 97 | "3170.jpg", 98 | "9140.jpg", 99 | "9181.jpg", 100 | "2301.jpg", 101 | "2799.jpg", 102 | "2800.jpg", 103 | "3230.jpg", 104 | "6312.jpg", 105 | "6821.jpg", 106 | "3550.1.jpg", 107 | "9043.jpg", 108 | "2683.jpg", 109 | "9561.jpg", 110 | "3300.jpg", 111 | "3053.jpg", 112 | "2053.jpg", 113 | "3180.jpg", 114 | "2751.jpg", 115 | "9600.jpg", 116 | "9570.jpg", 117 | "9295.jpg", 118 | "9410.jpg", 119 | "6825.jpg", 120 | "3195.jpg", 121 | "3160.jpg", 122 | "9342.jpg", 123 | "2345.1.jpg", 124 | "9810.jpg", 125 | "3064.jpg", 126 | "9265.jpg", 127 | "2141.jpg", 128 | "9185.jpg", 129 | "3120.jpg", 130 | "9413.jpg", 131 | "6563.jpg", 132 | "9325.jpg", 133 | "9635.1.jpg", 134 | "9041.jpg", 135 | "6243.jpg", 136 | "9921.jpg", 137 | "9831.jpg", 138 | "3015.jpg", 139 | "9560.jpg", 140 | "6571.jpg", 141 | "2710.jpg", 142 | "2205.jpg", 143 | "9415.jpg", 144 | "2900.jpg", 145 | "9520.jpg", 146 | "2730.jpg", 147 | "9424.jpg", 148 | "9800.jpg", 149 | "3400.jpg", 150 | "9301.jpg", 151 | "9332.jpg", 152 | "9075.jpg", 153 | "9254.jpg" 154 | ]; 155 | 156 | /* 157 | IMAGE OBJECTS 158 | { 159 | fileName: String representing file name (without path), 160 | valence: 'B' or 'N', 161 | dotPlacement: 'right' or 'left', 162 | greyDotX: Integer representing the x coordinate of the grey dot's position, 163 | greyDotY: Integer representing the y coordinate of the grey dot's position 164 | } 165 | */ 166 | 167 | /* IMAGE UTILS */ 168 | 169 | const getImagePath = ({ valence, fileName }) => { 170 | let folder; 171 | if (valence === "B") { 172 | folder = "negative"; 173 | } else if (valence === "N") { 174 | folder = "neutral"; 175 | } else if (valence === "S") { 176 | folder = "sample"; 177 | } 178 | return `assets/stimuli/${folder}/${fileName}`; 179 | }; 180 | 181 | /** 182 | * Returns an html tag to display the image. 183 | * @param {*} type 'N' for a neutral image, 'B' for a negative one 184 | * @param {*} current a list of the currently used images 185 | */ 186 | const generateImageHTML = image => ` 187 | 195 | `; 196 | 197 | // generates image HTML like above but with no grey dot 198 | const generateImageHTMLNoDot = image => ` 199 | 202 | `; 203 | 204 | const isNegativeImg = ({ valence }) => valence === "B"; 205 | 206 | const copyImage = img => ({ 207 | fileName: img.fileName, 208 | dotPlacement: img.dotPlacement, 209 | valence: img.valence, 210 | greyDotX: img.greyDotX, 211 | greyDotY: img.greyDotY 212 | }); 213 | 214 | const showFixationDot = timeline => { 215 | const whiteDot = { 216 | type: "html-keyboard-response", 217 | stimulus: 218 | '
',
219 | trial_duration: param.fixation_time * 1000,
220 | choices: jsPsych.NO_KEYS
221 | };
222 | timeline.push(whiteDot);
223 | };
224 |
225 | const showIntertrialBreak = timeline => {
226 | const whiteDot = {
227 | type: "html-keyboard-response",
228 | stimulus:
229 | 'This is the end of this set of images.
Please take a short break.
We will continue to show you images after the break ends.
Once again, please press "J" when you see the grey patch on the left side, press "K" when you see it on the right.
', 230 | trial_duration: param.break_duration * 1000, 231 | choices: jsPsych.NO_KEYS 232 | }; 233 | timeline.push(whiteDot); 234 | }; 235 | 236 | module.exports = { 237 | showIntertrialBreak, 238 | showFixationDot, 239 | generateImageHTML, 240 | generateImageHTMLNoDot, 241 | getImagePath, 242 | isNegativeImg, 243 | copyImage, 244 | neuImages, 245 | negImages 246 | }; 247 | -------------------------------------------------------------------------------- /assets/plugins/jspsych-html-double-slider-response.js: -------------------------------------------------------------------------------- 1 | jsPsych.plugins["html-double-slider-response"] = (function() { 2 | 3 | var plugin = {}; 4 | 5 | plugin.info = { 6 | name: "html-double-slider-response", 7 | parameters: { 8 | slider_count: { 9 | type: jsPsych.plugins.parameterType.INT, 10 | pretty_name: 'Slider Count', 11 | default: 2, 12 | description: 'Sets how many sliders will be displayed', 13 | }, 14 | stimuli: { 15 | type: jsPsych.plugins.parameterType.HTML_STRING, 16 | pretty_name: 'Stimuli', 17 | default: '', 18 | array: true, 19 | description: 'The HTML string to be displayed' 20 | }, 21 | default_stimuli: { 22 | type: jsPsych.plugins.parameterType.HTML_STRING, 23 | pretty_name: 'Default Stimuli', 24 | default: 'No default defined', 25 | array: false, 26 | description: 'The HTML string to be displayed' 27 | }, 28 | min: { 29 | type: jsPsych.plugins.parameterType.INT, 30 | pretty_name: 'Min slider', 31 | default: [0, 0], 32 | array: true, 33 | description: 'Sets the minimum value of the slider.' 34 | }, 35 | default_min: { 36 | type: jsPsych.plugins.parameterType.INT, 37 | pretty_name: 'Default Min slider', 38 | default: 0, 39 | description: 'Sets the minimum value of the slider.' 40 | }, 41 | max: { 42 | type: jsPsych.plugins.parameterType.INT, 43 | pretty_name: 'Max slider', 44 | default: [75, 150], 45 | array: true, 46 | description: 'Sets the maximum value of the slider', 47 | }, 48 | default_max: { 49 | type: jsPsych.plugins.parameterType.INT, 50 | pretty_name: 'Default Max slider', 51 | default: 100, 52 | array: false, 53 | description: 'Sets the minimum value of the slider.' 54 | }, 55 | start: { 56 | type: jsPsych.plugins.parameterType.INT, 57 | pretty_name: 'Slider starting value', 58 | default: [25, 100], 59 | array: true, 60 | description: 'Sets the starting value of the slider', 61 | }, 62 | default_start: { 63 | type: jsPsych.plugins.parameterType.INT, 64 | pretty_name: 'Default Min slider', 65 | default: 0, 66 | array: false, 67 | description: 'Sets the minimum value of the slider.' 68 | }, 69 | step: { 70 | type: jsPsych.plugins.parameterType.INT, 71 | pretty_name: 'Step', 72 | default: [5, 1], 73 | array: true, 74 | description: 'Sets the step of the slider' 75 | }, 76 | default_step: { 77 | type: jsPsych.plugins.parameterType.INT, 78 | pretty_name: 'Default Step', 79 | default: 1, 80 | array: false, 81 | description: 'Sets the step of the slider' 82 | }, 83 | labels: { 84 | type: jsPsych.plugins.parameterType.HTML_STRING, 85 | pretty_name:'Labels', 86 | default: [], 87 | array: true, 88 | description: 'Labels of the slider.', 89 | }, 90 | default_labels: { 91 | type: jsPsych.plugins.parameterType.INT, 92 | pretty_name: 'Default Step', 93 | default: [], 94 | array: true, 95 | description: 'Sets the step of the slider' 96 | }, 97 | button_label: { 98 | type: jsPsych.plugins.parameterType.STRING, 99 | pretty_name: 'Button label', 100 | default: 'Continue', 101 | array: false, 102 | description: 'Label of the button to advance.' 103 | }, 104 | prompt: { 105 | type: jsPsych.plugins.parameterType.STRING, 106 | pretty_name: 'Prompt', 107 | default: [], 108 | array: true, 109 | description: 'Any content here will be displayed below the slider.' 110 | }, 111 | default_prompt: { 112 | type: jsPsych.plugins.parameterType.STRING, 113 | pretty_name: 'Prompt', 114 | default: '', 115 | array: false, 116 | description: 'Any content here will be displayed below the slider.' 117 | }, 118 | stimulus_duration: { 119 | type: jsPsych.plugins.parameterType.INT, 120 | pretty_name: 'Stimulus duration', 121 | default: null, 122 | description: 'How long to hide the stimulus.' 123 | }, 124 | trial_duration: { 125 | type: jsPsych.plugins.parameterType.INT, 126 | pretty_name: 'Trial duration', 127 | default: null, 128 | description: 'How long to show the trial.' 129 | }, 130 | response_ends_trial: { 131 | type: jsPsych.plugins.parameterType.BOOL, 132 | pretty_name: 'Response ends trial', 133 | default: true, 134 | description: 'If true, trial will end when user makes a response.' 135 | }, 136 | } 137 | } 138 | 139 | plugin.trial = function(display_element, trial) { 140 | 141 | var html = ''; 142 | 143 | // Display trial.slider_count amount of sliders 144 | for(var i = 0; i < trial.slider_count; i++) { 145 | html += '