@decidables/accumulable-elements 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE.md +1003 -0
- package/README.md +1048 -0
- package/lib/accumulableElements.esm.js +10574 -0
- package/lib/accumulableElements.esm.js.map +1 -0
- package/lib/accumulableElements.esm.min.js +1917 -0
- package/lib/accumulableElements.esm.min.js.map +1 -0
- package/lib/accumulableElements.umd.js +10594 -0
- package/lib/accumulableElements.umd.js.map +1 -0
- package/lib/accumulableElements.umd.min.js +1917 -0
- package/lib/accumulableElements.umd.min.js.map +1 -0
- package/package.json +65 -0
- package/src/accumulable-element.js +87 -0
- package/src/components/accumulable-control.js +249 -0
- package/src/components/accumulable-response.js +428 -0
- package/src/components/accumulable-table.js +520 -0
- package/src/components/ddm-fit-worker.js +31 -0
- package/src/components/ddm-fit.js +130 -0
- package/src/components/ddm-model.js +2095 -0
- package/src/components/ddm-parameters.js +139 -0
- package/src/components/index.js +8 -0
- package/src/components/rdk-2afc-task.js +537 -0
- package/src/equations/azv2pc.js +146 -0
- package/src/equations/azvt02m.js +182 -0
- package/src/equations/ddm-equation.js +193 -0
- package/src/equations/index.js +3 -0
- package/src/examples/ddm-example.js +72 -0
- package/src/examples/human.js +208 -0
- package/src/examples/index.js +4 -0
- package/src/examples/interactive.js +150 -0
- package/src/examples/model.js +307 -0
- package/src/index.js +6 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
|
|
2
|
+
import {html, css} from 'lit';
|
|
3
|
+
|
|
4
|
+
import '@decidables/decidables-elements/slider';
|
|
5
|
+
|
|
6
|
+
import AccumulableElement from '../accumulable-element';
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
DDMParameters element
|
|
10
|
+
<ddm-paramters>
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
|
|
14
|
+
*/
|
|
15
|
+
export default class DDMParameters extends AccumulableElement {
|
|
16
|
+
static get properties() {
|
|
17
|
+
return {
|
|
18
|
+
a: {
|
|
19
|
+
attribute: 'boundary-separation',
|
|
20
|
+
type: Number,
|
|
21
|
+
reflect: true,
|
|
22
|
+
},
|
|
23
|
+
z: {
|
|
24
|
+
attribute: 'starting-point',
|
|
25
|
+
type: Number,
|
|
26
|
+
reflect: true,
|
|
27
|
+
},
|
|
28
|
+
v: {
|
|
29
|
+
attribute: 'drift-rate',
|
|
30
|
+
type: Number,
|
|
31
|
+
reflect: true,
|
|
32
|
+
},
|
|
33
|
+
t0: {
|
|
34
|
+
attribute: 'nondecision-time',
|
|
35
|
+
type: Number,
|
|
36
|
+
reflect: true,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
constructor() {
|
|
42
|
+
super();
|
|
43
|
+
|
|
44
|
+
// Attributes
|
|
45
|
+
this.a = undefined;
|
|
46
|
+
this.z = undefined;
|
|
47
|
+
this.v = undefined;
|
|
48
|
+
this.t0 = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setBoundarySeparation(e) {
|
|
52
|
+
this.a = +e.target.value;
|
|
53
|
+
this.dispatchEvent(new CustomEvent('ddm-parameters-a', {
|
|
54
|
+
detail: {
|
|
55
|
+
a: this.a,
|
|
56
|
+
},
|
|
57
|
+
bubbles: true,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setStartingPoint(e) {
|
|
62
|
+
this.z = +e.target.value;
|
|
63
|
+
this.dispatchEvent(new CustomEvent('ddm-parameters-z', {
|
|
64
|
+
detail: {
|
|
65
|
+
z: this.z,
|
|
66
|
+
},
|
|
67
|
+
bubbles: true,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
setDriftRate(e) {
|
|
72
|
+
this.v = +e.target.value;
|
|
73
|
+
this.dispatchEvent(new CustomEvent('ddm-parameters-v', {
|
|
74
|
+
detail: {
|
|
75
|
+
v: this.v,
|
|
76
|
+
},
|
|
77
|
+
bubbles: true,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setNondecisionTime(e) {
|
|
82
|
+
this.t0 = +e.target.value;
|
|
83
|
+
this.dispatchEvent(new CustomEvent('ddm-parameters-t0', {
|
|
84
|
+
detail: {
|
|
85
|
+
t0: this.t0,
|
|
86
|
+
},
|
|
87
|
+
bubbles: true,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static get styles() {
|
|
92
|
+
return [
|
|
93
|
+
super.styles,
|
|
94
|
+
css`
|
|
95
|
+
:host {
|
|
96
|
+
display: inline-block;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.holder {
|
|
100
|
+
display: flex;
|
|
101
|
+
|
|
102
|
+
flex-direction: row;
|
|
103
|
+
|
|
104
|
+
align-items: stretch;
|
|
105
|
+
justify-content: center;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
decidables-slider {
|
|
109
|
+
line-height: 1;
|
|
110
|
+
text-align: center;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
decidables-slider div {
|
|
114
|
+
margin-bottom: 0.25rem;
|
|
115
|
+
}
|
|
116
|
+
`,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render() {
|
|
121
|
+
return html`
|
|
122
|
+
<div class="holder">
|
|
123
|
+
${this.a != null
|
|
124
|
+
? html`<decidables-slider class="a" ?disabled=${!this.interactive} min="0.01" max="2" step="0.01" .value=${+this.a.toFixed(2)} @change=${this.setBoundarySeparation.bind(this)} @input=${this.setBoundarySeparation.bind(this)}><div>Boundary Separation<br><span class="math-var">a</span></div></decidables-slider>`
|
|
125
|
+
: html``}
|
|
126
|
+
${this.z != null
|
|
127
|
+
? html`<decidables-slider class="z" ?disabled=${!this.interactive} min="0.01" max="0.99" step="0.01" .value=${+this.z.toFixed(2)} @change=${this.setStartingPoint.bind(this)} @input=${this.setStartingPoint.bind(this)}><div>Starting Point<br><span class="math-var">z</span></div></decidables-slider>`
|
|
128
|
+
: html``}
|
|
129
|
+
${this.v != null
|
|
130
|
+
? html`<decidables-slider class="v" ?disabled=${!this.interactive} min="0.01" max="5" step="0.01" .value=${+this.v.toFixed(2)} @change=${this.setDriftRate.bind(this)} @input=${this.setDriftRate.bind(this)}><div>Drift Rate<br><span class="math-var">v</span></div></decidables-slider>`
|
|
131
|
+
: html``}
|
|
132
|
+
${this.t0 != null
|
|
133
|
+
? html`<decidables-slider class="t0" ?disabled=${!this.interactive} min="0" max="500" step="1" .value=${+this.t0.toFixed(0)} @change=${this.setNondecisionTime.bind(this)} @input=${this.setNondecisionTime.bind(this)}><div>Nondecision Time<br><span class="math-var">t₀</span></div></decidables-slider>`
|
|
134
|
+
: html``}
|
|
135
|
+
</div>`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
customElements.define('ddm-parameters', DDMParameters);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
export {default as AccumulableControl} from './accumulable-control';
|
|
3
|
+
export {default as AccumulableResponse} from './accumulable-response';
|
|
4
|
+
export {default as AccumulableTable} from './accumulable-table';
|
|
5
|
+
export {default as DDMFit} from './ddm-fit';
|
|
6
|
+
export {default as DDMParameters} from './ddm-parameters';
|
|
7
|
+
export {default as DDMModel} from './ddm-model';
|
|
8
|
+
export {default as RDK2AFCTask} from './rdk-2afc-task';
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
|
|
2
|
+
import {html, css} from 'lit';
|
|
3
|
+
import * as d3 from 'd3';
|
|
4
|
+
|
|
5
|
+
import {DecidablesMixinResizeable} from '@decidables/decidables-elements';
|
|
6
|
+
|
|
7
|
+
import AccumulableElement from '../accumulable-element';
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
RDK2AFCTask element
|
|
11
|
+
<rdk-2afc-task>
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
Dots; Coherence;
|
|
15
|
+
# Direction, Speed, Lifetime
|
|
16
|
+
*/
|
|
17
|
+
export default class RDK2AFCTask extends DecidablesMixinResizeable(AccumulableElement) {
|
|
18
|
+
static get properties() {
|
|
19
|
+
return {
|
|
20
|
+
coherence: {
|
|
21
|
+
attribute: 'coherence',
|
|
22
|
+
type: Number,
|
|
23
|
+
reflect: true,
|
|
24
|
+
},
|
|
25
|
+
count: {
|
|
26
|
+
attribute: 'count',
|
|
27
|
+
type: Number,
|
|
28
|
+
reflect: true,
|
|
29
|
+
},
|
|
30
|
+
probability: {
|
|
31
|
+
attribute: 'probability',
|
|
32
|
+
type: Number,
|
|
33
|
+
reflect: true,
|
|
34
|
+
},
|
|
35
|
+
duration: {
|
|
36
|
+
attribute: 'duration',
|
|
37
|
+
type: Number,
|
|
38
|
+
reflect: true,
|
|
39
|
+
},
|
|
40
|
+
wait: {
|
|
41
|
+
attribute: 'wait',
|
|
42
|
+
type: Number,
|
|
43
|
+
reflect: true,
|
|
44
|
+
},
|
|
45
|
+
iti: {
|
|
46
|
+
attribute: 'iti',
|
|
47
|
+
type: Number,
|
|
48
|
+
reflect: true,
|
|
49
|
+
},
|
|
50
|
+
trials: {
|
|
51
|
+
attribute: 'trials',
|
|
52
|
+
type: Number,
|
|
53
|
+
reflect: true,
|
|
54
|
+
},
|
|
55
|
+
running: {
|
|
56
|
+
attribute: 'running',
|
|
57
|
+
type: Boolean,
|
|
58
|
+
reflect: true,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
lifetime: {
|
|
62
|
+
attribute: false,
|
|
63
|
+
type: Number,
|
|
64
|
+
reflect: false,
|
|
65
|
+
},
|
|
66
|
+
speed: {
|
|
67
|
+
attribute: false,
|
|
68
|
+
type: Number,
|
|
69
|
+
reflect: false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
constructor() {
|
|
75
|
+
super();
|
|
76
|
+
|
|
77
|
+
// Attributes
|
|
78
|
+
this.coherence = 0.5; // Proportion of dots moving coherently
|
|
79
|
+
this.count = 100; // Number of dots
|
|
80
|
+
this.probability = 0.5; // Probability of left (as opposed to right)
|
|
81
|
+
this.duration = 2000; // Duration of stimulus in milliseconds
|
|
82
|
+
this.wait = 2000; // Duration of wait period for response in milliseconds
|
|
83
|
+
this.iti = 2000; // Duration of inter-trial interval in milliseconds
|
|
84
|
+
this.trials = 5; // Number of trials per block
|
|
85
|
+
this.running = false; // Currently executing block of trials
|
|
86
|
+
|
|
87
|
+
// Properties
|
|
88
|
+
this.lifetime = 400; // Lifetime of each dot in milliseconds
|
|
89
|
+
this.speed = 50; // Rate of dot movement in pixels per second
|
|
90
|
+
|
|
91
|
+
// Private
|
|
92
|
+
this.firstUpdate = true;
|
|
93
|
+
this.COHERENT = 0; // "Constant" for index to coherent dots
|
|
94
|
+
this.RANDOM = 1; // "Constant" for index to random dots
|
|
95
|
+
this.dots = [[], []]; // Array of array of dots
|
|
96
|
+
this.trial = 0; // Count of current trial
|
|
97
|
+
|
|
98
|
+
this.states = ['resetted', 'iti', 'stimulus', 'wait', 'ended']; // Possible states of task
|
|
99
|
+
this.state = 'resetted'; // Current state of task
|
|
100
|
+
|
|
101
|
+
this.baseTime = 0; // Real time, in milliseconds, that the current block started
|
|
102
|
+
this.pauseTime = 0; // Real time, in milliseconds, that block was paused at
|
|
103
|
+
this.startTime = 0; // Virtual time, in milliseconds, that current stage of trial started
|
|
104
|
+
this.lastTime = 0; // Virtual time, in milliseconds, of the most recent frame
|
|
105
|
+
|
|
106
|
+
this.LEFT = 180; // "Constant" for left stimulus direction
|
|
107
|
+
this.RIGHT = 0; // "Constant" for right stimulus direction
|
|
108
|
+
this.currentDirection = undefined; // Direction in degrees for current trial
|
|
109
|
+
|
|
110
|
+
this.signals = ['left', 'right']; // Possible trial types
|
|
111
|
+
this.signal = undefined; // Current trial type
|
|
112
|
+
|
|
113
|
+
this.runner = undefined; // D3 Interval for frame timing
|
|
114
|
+
this.xScale = undefined; // D3 Scale for x-axis
|
|
115
|
+
this.yScale = undefined; // D3 Scale for y-axis
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
static get styles() {
|
|
119
|
+
return [
|
|
120
|
+
super.styles,
|
|
121
|
+
css`
|
|
122
|
+
:host {
|
|
123
|
+
display: inline-block;
|
|
124
|
+
|
|
125
|
+
width: 10rem;
|
|
126
|
+
height: 10rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.main {
|
|
130
|
+
width: 100%;
|
|
131
|
+
height: 100%;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.background {
|
|
135
|
+
fill: var(---color-element-disabled);
|
|
136
|
+
stroke: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.outline {
|
|
140
|
+
fill: none;
|
|
141
|
+
stroke: var(---color-element-emphasis);
|
|
142
|
+
stroke-width: 2px;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.dot {
|
|
146
|
+
r: 2px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.dots.coherent {
|
|
150
|
+
fill: var(---color-background);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.dots.random {
|
|
154
|
+
fill: var(---color-background);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.fixation {
|
|
158
|
+
stroke: var(---color-text);
|
|
159
|
+
stroke-width: 2px;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.query {
|
|
163
|
+
font-size: 1.75rem;
|
|
164
|
+
font-weight: 600;
|
|
165
|
+
}
|
|
166
|
+
`,
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
render() { /* eslint-disable-line class-methods-use-this */
|
|
171
|
+
return html``;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
update(changedProperties) {
|
|
175
|
+
super.update(changedProperties);
|
|
176
|
+
|
|
177
|
+
// Bail out if we can't get the width/height/rem
|
|
178
|
+
if (Number.isNaN(this.width) || Number.isNaN(this.height) || Number.isNaN(this.rem)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const elementWidth = this.width;
|
|
183
|
+
const elementHeight = this.height;
|
|
184
|
+
const elementSize = Math.min(elementWidth, elementHeight);
|
|
185
|
+
|
|
186
|
+
const margin = {
|
|
187
|
+
top: 0.25 * this.rem,
|
|
188
|
+
bottom: 0.25 * this.rem,
|
|
189
|
+
left: 0.25 * this.rem,
|
|
190
|
+
right: 0.25 * this.rem,
|
|
191
|
+
};
|
|
192
|
+
const height = elementSize - (margin.top + margin.bottom);
|
|
193
|
+
const width = elementSize - (margin.left + margin.right);
|
|
194
|
+
|
|
195
|
+
// X Scale
|
|
196
|
+
this.xScale = d3.scaleLinear()
|
|
197
|
+
.domain([-1, 1])
|
|
198
|
+
.range([0, width]);
|
|
199
|
+
|
|
200
|
+
// Y Scale
|
|
201
|
+
this.yScale = d3.scaleLinear()
|
|
202
|
+
.domain([1, -1])
|
|
203
|
+
.range([0, height]);
|
|
204
|
+
|
|
205
|
+
// Svg
|
|
206
|
+
// DATA-JOIN
|
|
207
|
+
const svgUpdate = d3.select(this.renderRoot).selectAll('.main')
|
|
208
|
+
.data([{
|
|
209
|
+
width: this.width,
|
|
210
|
+
height: this.height,
|
|
211
|
+
rem: this.rem,
|
|
212
|
+
}]);
|
|
213
|
+
// ENTER
|
|
214
|
+
const svgEnter = svgUpdate.enter().append('svg')
|
|
215
|
+
.classed('main', true);
|
|
216
|
+
// MERGE
|
|
217
|
+
const svgMerge = svgEnter.merge(svgUpdate)
|
|
218
|
+
.attr('viewBox', `0 0 ${elementSize} ${elementSize}`);
|
|
219
|
+
|
|
220
|
+
// Clippath
|
|
221
|
+
// ENTER
|
|
222
|
+
svgEnter.append('clipPath')
|
|
223
|
+
.attr('id', 'clip-rdk-task')
|
|
224
|
+
.append('circle');
|
|
225
|
+
// MERGE
|
|
226
|
+
svgMerge.select('clipPath circle')
|
|
227
|
+
.attr('cx', this.xScale(0))
|
|
228
|
+
.attr('cy', this.yScale(0))
|
|
229
|
+
.attr('r', this.xScale(1) - this.xScale(0));
|
|
230
|
+
|
|
231
|
+
// Plot
|
|
232
|
+
// ENTER
|
|
233
|
+
const plotEnter = svgEnter.append('g')
|
|
234
|
+
.classed('plot', true);
|
|
235
|
+
// MERGE
|
|
236
|
+
const plotMerge = svgMerge.select('.plot')
|
|
237
|
+
.attr('transform', `translate(${margin.left}, ${margin.top})`);
|
|
238
|
+
|
|
239
|
+
// Underlayer
|
|
240
|
+
// ENTER
|
|
241
|
+
const underlayerEnter = plotEnter.append('g')
|
|
242
|
+
.classed('underlayer', true);
|
|
243
|
+
// MERGE
|
|
244
|
+
const underlayerMerge = plotMerge.select('.underlayer');
|
|
245
|
+
|
|
246
|
+
// Background
|
|
247
|
+
// ENTER
|
|
248
|
+
underlayerEnter.append('circle')
|
|
249
|
+
.classed('background', true);
|
|
250
|
+
// MERGE
|
|
251
|
+
underlayerMerge.select('.background')
|
|
252
|
+
.attr('cx', this.xScale(0))
|
|
253
|
+
.attr('cy', this.yScale(0))
|
|
254
|
+
.attr('r', this.xScale(1) - this.xScale(0));
|
|
255
|
+
|
|
256
|
+
// Content
|
|
257
|
+
// ENTER
|
|
258
|
+
plotEnter.append('g')
|
|
259
|
+
.classed('content', true)
|
|
260
|
+
.attr('clip-path', 'url(#clip-rdk-task)');
|
|
261
|
+
// MERGE
|
|
262
|
+
const contentMerge = plotMerge.select('.content');
|
|
263
|
+
|
|
264
|
+
// Dot Groups
|
|
265
|
+
// DATA-JOIN
|
|
266
|
+
const dotsUpdate = contentMerge.selectAll('.dots')
|
|
267
|
+
.data([[], []]);
|
|
268
|
+
// ENTER
|
|
269
|
+
dotsUpdate.enter().append('g')
|
|
270
|
+
.classed('dots', true)
|
|
271
|
+
.classed('coherent', (datum, index) => { return index === this.COHERENT; })
|
|
272
|
+
.classed('random', (datum, index) => { return index === this.RANDOM; });
|
|
273
|
+
|
|
274
|
+
// Overlayer
|
|
275
|
+
// ENTER
|
|
276
|
+
const overlayerEnter = plotEnter.append('g')
|
|
277
|
+
.classed('overlayer', true);
|
|
278
|
+
// MERGE
|
|
279
|
+
const overlayerMerge = plotMerge.select('.overlayer');
|
|
280
|
+
|
|
281
|
+
// Outline
|
|
282
|
+
// ENTER
|
|
283
|
+
overlayerEnter.append('circle')
|
|
284
|
+
.classed('outline', true);
|
|
285
|
+
// MERGE
|
|
286
|
+
overlayerMerge.select('.outline')
|
|
287
|
+
.attr('cx', this.xScale(0))
|
|
288
|
+
.attr('cy', this.yScale(0))
|
|
289
|
+
.attr('r', this.xScale(1) - this.yScale(0));
|
|
290
|
+
|
|
291
|
+
// Start or stop trial block
|
|
292
|
+
if (this.firstUpdate || changedProperties.has('running')) {
|
|
293
|
+
if (this.running) {
|
|
294
|
+
// (Re)Start
|
|
295
|
+
if (this.pauseTime) {
|
|
296
|
+
// Shift timeline forward as if paused time never happened
|
|
297
|
+
this.baseTime += (d3.now() - this.pauseTime);
|
|
298
|
+
this.pauseTime = 0;
|
|
299
|
+
}
|
|
300
|
+
this.runner = d3.interval(this.run.bind(this), 20); // FIXME??
|
|
301
|
+
} else if (this.runner !== undefined) {
|
|
302
|
+
// Pause
|
|
303
|
+
this.runner.stop();
|
|
304
|
+
this.pauseTime = d3.now();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
this.firstUpdate = false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
reset() {
|
|
312
|
+
if (this.runner !== undefined) {
|
|
313
|
+
this.runner.stop();
|
|
314
|
+
}
|
|
315
|
+
this.running = false;
|
|
316
|
+
this.trial = 0;
|
|
317
|
+
this.state = 'resetted';
|
|
318
|
+
this.baseTime = 0;
|
|
319
|
+
this.pauseTime = 0;
|
|
320
|
+
this.startTime = 0;
|
|
321
|
+
this.lastTime = 0;
|
|
322
|
+
this.signal = undefined;
|
|
323
|
+
this.currentDirection = undefined;
|
|
324
|
+
|
|
325
|
+
const dotsUpdate = d3.select(this.renderRoot).select('.content').selectAll('.dots')
|
|
326
|
+
.data([[], []]);
|
|
327
|
+
const dotUpdate = dotsUpdate.selectAll('.dot')
|
|
328
|
+
.data((datum) => { return datum; });
|
|
329
|
+
dotUpdate.exit()
|
|
330
|
+
.remove();
|
|
331
|
+
|
|
332
|
+
const fixationUpdate = d3.select(this.renderRoot).select('.content').selectAll('.fixation')
|
|
333
|
+
.data([]);
|
|
334
|
+
fixationUpdate.exit()
|
|
335
|
+
.remove();
|
|
336
|
+
const queryUpdate = d3.select(this.renderRoot).select('.content').selectAll('.query')
|
|
337
|
+
.data([]);
|
|
338
|
+
queryUpdate.exit()
|
|
339
|
+
.remove();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
run(/* elapsed */) {
|
|
343
|
+
const realTime = d3.now();
|
|
344
|
+
const currentTime = (this.baseTime) ? (realTime - this.baseTime) : 0;
|
|
345
|
+
const elapsedTime = (this.baseTime) ? (currentTime - this.startTime) : 0;
|
|
346
|
+
const frameTime = (this.baseTime) ? (currentTime - this.lastTime) : 0;
|
|
347
|
+
this.lastTime = currentTime;
|
|
348
|
+
let newTrial = false;
|
|
349
|
+
if (this.state === 'resetted') {
|
|
350
|
+
// Start block with an ITI
|
|
351
|
+
this.state = 'iti';
|
|
352
|
+
this.baseTime = realTime;
|
|
353
|
+
this.startTime = 0;
|
|
354
|
+
this.dispatchEvent(new CustomEvent('rdk-block-start', {
|
|
355
|
+
detail: {
|
|
356
|
+
trials: this.trials,
|
|
357
|
+
},
|
|
358
|
+
bubbles: true,
|
|
359
|
+
}));
|
|
360
|
+
} else if ((this.state === 'iti') && (elapsedTime >= this.iti)) {
|
|
361
|
+
// Start new trial with a stimulus
|
|
362
|
+
newTrial = true;
|
|
363
|
+
this.trial += 1;
|
|
364
|
+
this.state = 'stimulus';
|
|
365
|
+
this.startTime = currentTime;
|
|
366
|
+
this.signal = (Math.random() < this.probability) ? 'left' : 'right';
|
|
367
|
+
this.currentDirection = (this.signal === 'left')
|
|
368
|
+
? this.LEFT
|
|
369
|
+
: this.RIGHT;
|
|
370
|
+
this.dispatchEvent(new CustomEvent('rdk-trial-start', {
|
|
371
|
+
detail: {
|
|
372
|
+
trials: this.trials,
|
|
373
|
+
duration: this.duration,
|
|
374
|
+
wait: this.wait,
|
|
375
|
+
iti: this.iti,
|
|
376
|
+
trial: this.trial,
|
|
377
|
+
signal: this.signal,
|
|
378
|
+
},
|
|
379
|
+
bubbles: true,
|
|
380
|
+
}));
|
|
381
|
+
} else if ((this.state === 'stimulus') && (elapsedTime >= this.duration)) {
|
|
382
|
+
// Stimulus is over, now wait
|
|
383
|
+
this.state = 'wait';
|
|
384
|
+
this.startTime = currentTime;
|
|
385
|
+
this.dispatchEvent(new CustomEvent('rdk-trial-middle', {
|
|
386
|
+
detail: {
|
|
387
|
+
trials: this.trials,
|
|
388
|
+
duration: this.duration,
|
|
389
|
+
wait: this.wait,
|
|
390
|
+
iti: this.iti,
|
|
391
|
+
trial: this.trial,
|
|
392
|
+
signal: this.signal,
|
|
393
|
+
},
|
|
394
|
+
bubbles: true,
|
|
395
|
+
}));
|
|
396
|
+
} else if ((this.state === 'wait') && (elapsedTime >= this.wait)) {
|
|
397
|
+
// Wait is over, end of trial
|
|
398
|
+
this.dispatchEvent(new CustomEvent('rdk-trial-end', {
|
|
399
|
+
detail: {
|
|
400
|
+
trials: this.trials,
|
|
401
|
+
duration: this.duration,
|
|
402
|
+
wait: this.wait,
|
|
403
|
+
iti: this.iti,
|
|
404
|
+
trial: this.trial,
|
|
405
|
+
signal: this.signal,
|
|
406
|
+
},
|
|
407
|
+
bubbles: true,
|
|
408
|
+
}));
|
|
409
|
+
if (this.trial >= this.trials) {
|
|
410
|
+
// End of block
|
|
411
|
+
this.runner.stop();
|
|
412
|
+
this.running = false;
|
|
413
|
+
this.state = 'ended';
|
|
414
|
+
this.baseTime = 0;
|
|
415
|
+
this.pauseTime = 0;
|
|
416
|
+
this.startTime = 0;
|
|
417
|
+
this.lastTime = 0;
|
|
418
|
+
this.signal = undefined;
|
|
419
|
+
this.currentDirection = undefined;
|
|
420
|
+
this.dispatchEvent(new CustomEvent('rdk-block-end', {
|
|
421
|
+
detail: {
|
|
422
|
+
trials: this.trial,
|
|
423
|
+
},
|
|
424
|
+
bubbles: true,
|
|
425
|
+
}));
|
|
426
|
+
} else {
|
|
427
|
+
// ITI
|
|
428
|
+
this.state = 'iti';
|
|
429
|
+
this.startTime = currentTime;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Dots
|
|
434
|
+
if (this.state === 'stimulus') {
|
|
435
|
+
this.dots[this.COHERENT].length = Math.round(this.count * this.coherence);
|
|
436
|
+
this.dots[this.RANDOM].length = (this.count - this.dots[this.COHERENT].length);
|
|
437
|
+
|
|
438
|
+
for (let t = 0; t < this.dots.length; t += 1) {
|
|
439
|
+
for (let i = 0; i < this.dots[t].length; i += 1) {
|
|
440
|
+
const newDot = (this.dots[t][i] === undefined);
|
|
441
|
+
if (newDot) {
|
|
442
|
+
this.dots[t][i] = {};
|
|
443
|
+
}
|
|
444
|
+
const dot = this.dots[t][i];
|
|
445
|
+
if (newTrial || newDot) {
|
|
446
|
+
dot.direction = (t === this.RANDOM) ? (Math.random() * 360) : this.currentDirection;
|
|
447
|
+
dot.birth = currentTime - Math.floor(Math.random() * this.lifetime);
|
|
448
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
449
|
+
const radius = Math.sqrt(Math.random());
|
|
450
|
+
dot.x = this.xScale(radius * Math.cos(angle));
|
|
451
|
+
dot.y = this.yScale(radius * Math.sin(angle));
|
|
452
|
+
} else if (currentTime > (dot.birth + this.lifetime)) {
|
|
453
|
+
// Dot has died, so rebirth
|
|
454
|
+
dot.birth += this.lifetime;
|
|
455
|
+
dot.direction = (t === this.RANDOM) ? (Math.random() * 360) : this.currentDirection;
|
|
456
|
+
const angle = Math.random() * 2 * Math.PI;
|
|
457
|
+
const radius = Math.sqrt(Math.random());
|
|
458
|
+
dot.x = this.xScale(radius * Math.cos(angle));
|
|
459
|
+
dot.y = this.yScale(radius * Math.sin(angle));
|
|
460
|
+
} else {
|
|
461
|
+
if (t === this.COHERENT) {
|
|
462
|
+
dot.direction = this.currentDirection;
|
|
463
|
+
}
|
|
464
|
+
const directionR = dot.direction * (Math.PI / 180);
|
|
465
|
+
dot.dx = this.speed * (frameTime / 1000) * Math.cos(directionR);
|
|
466
|
+
dot.dy = this.speed * (frameTime / 1000) * Math.sin(directionR);
|
|
467
|
+
// Update position
|
|
468
|
+
dot.x += dot.dx;
|
|
469
|
+
dot.y += dot.dy;
|
|
470
|
+
// Calculate squared distance from center
|
|
471
|
+
const distance2 = ((dot.x - this.xScale(0)) ** 2) + ((dot.y - this.yScale(0)) ** 2);
|
|
472
|
+
const radius2 = (this.xScale(1) - this.xScale(0)) ** 2;
|
|
473
|
+
if (distance2 > radius2) {
|
|
474
|
+
// Dot has exited so move to other side
|
|
475
|
+
dot.x = -(dot.x - this.xScale(0)) + this.xScale(0);
|
|
476
|
+
dot.y = -(dot.y - this.yScale(0)) + this.yScale(0);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Fixation
|
|
484
|
+
// DATA-JOIN
|
|
485
|
+
const fixationUpdate = d3.select(this.renderRoot).select('.content').selectAll('.fixation')
|
|
486
|
+
.data((this.state === 'iti') ? [true] : []);
|
|
487
|
+
// ENTER
|
|
488
|
+
const fixationEnter = fixationUpdate.enter().append('g')
|
|
489
|
+
.classed('fixation', true);
|
|
490
|
+
fixationEnter.append('line')
|
|
491
|
+
.attr('x1', this.xScale(-0.1))
|
|
492
|
+
.attr('y1', this.xScale(0))
|
|
493
|
+
.attr('x2', this.xScale(0.1))
|
|
494
|
+
.attr('y2', this.xScale(0));
|
|
495
|
+
fixationEnter.append('line')
|
|
496
|
+
.attr('x1', this.xScale(0))
|
|
497
|
+
.attr('y1', this.xScale(-0.1))
|
|
498
|
+
.attr('x2', this.xScale(0))
|
|
499
|
+
.attr('y2', this.xScale(0.1));
|
|
500
|
+
// EXIT
|
|
501
|
+
fixationUpdate.exit().remove();
|
|
502
|
+
|
|
503
|
+
// Dots
|
|
504
|
+
// DATA-JOIN
|
|
505
|
+
const dotsUpdate = d3.select(this.renderRoot).select('.content').selectAll('.dots')
|
|
506
|
+
.data((this.state === 'stimulus') ? this.dots : [[], []]);
|
|
507
|
+
const dotUpdate = dotsUpdate.selectAll('.dot')
|
|
508
|
+
.data((datum) => { return datum; });
|
|
509
|
+
// ENTER
|
|
510
|
+
const dotEnter = dotUpdate.enter().append('circle')
|
|
511
|
+
.classed('dot', true);
|
|
512
|
+
// MERGE
|
|
513
|
+
dotEnter.merge(dotUpdate)
|
|
514
|
+
.attr('cx', (datum) => { return datum.x; })
|
|
515
|
+
.attr('cy', (datum) => { return datum.y; });
|
|
516
|
+
// EXIT
|
|
517
|
+
dotUpdate.exit().remove();
|
|
518
|
+
|
|
519
|
+
// Query
|
|
520
|
+
// DATA-JOIN
|
|
521
|
+
const queryUpdate = d3.select(this.renderRoot).select('.content').selectAll('.query')
|
|
522
|
+
.data((this.state === 'wait') ? [true] : []);
|
|
523
|
+
// ENTER
|
|
524
|
+
const queryEnter = queryUpdate.enter().append('g')
|
|
525
|
+
.classed('query', true);
|
|
526
|
+
queryEnter.append('text')
|
|
527
|
+
.attr('x', this.xScale(0))
|
|
528
|
+
.attr('y', this.xScale(0))
|
|
529
|
+
.attr('text-anchor', 'middle')
|
|
530
|
+
.attr('alignment-baseline', 'middle')
|
|
531
|
+
.text('?');
|
|
532
|
+
// EXIT
|
|
533
|
+
queryUpdate.exit().remove();
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
customElements.define('rdk-2afc-task', RDK2AFCTask);
|