Merge pull request #2577 from jspsych/plugin-virtual-chinrest

Remove SVG.js dependency from virtual-chinrest
This commit is contained in:
Josh de Leeuw 2022-04-08 13:23:57 -04:00 committed by GitHub
commit 58fdfd9529
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 366 additions and 106 deletions

View File

@ -0,0 +1,11 @@
---
"@jspsych/plugin-virtual-chinrest": major
---
Several changes to this plugin:
* Removed dependency on svg.js
* Added a reset between each blind spot measurement for clarity
* Moved starting location of square and dot to right edge of screen for maximum compatibility with large viewing distances
* Makes image of credit card optional
* Refactored code

View File

@ -11,14 +11,6 @@ The plugin works in two phases.
**Phase 2**. To measure the participant's viewing distance from their screen we use a [blind spot](<https://en.wikipedia.org/wiki/Blind_spot_(vision)>) task. Participants are asked to focus on a black square on the screen with their right eye closed, while a red dot repeatedly sweeps from right to left. They press the spacebar on their keyboard whenever they perceive that the red dot has disappeared. This part allows the plugin to use the distance between the black square and the red dot when it disappears from eyesight to estimate how far the participant is from the monitor. This estimation assumes that the blind spot is located at 13.5° temporally. **Phase 2**. To measure the participant's viewing distance from their screen we use a [blind spot](<https://en.wikipedia.org/wiki/Blind_spot_(vision)>) task. Participants are asked to focus on a black square on the screen with their right eye closed, while a red dot repeatedly sweeps from right to left. They press the spacebar on their keyboard whenever they perceive that the red dot has disappeared. This part allows the plugin to use the distance between the black square and the red dot when it disappears from eyesight to estimate how far the participant is from the monitor. This estimation assumes that the blind spot is located at 13.5° temporally.
## Dependency
This plugin requires the SVG.js library, available at [https://svgjs.com](https://svgjs.com/docs/3.0/) or via the CDN link below. You must include the library in the `<head>` section of your experiment page.
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/svg.js/2.6.3/svg.min.js"></script>
```
## Parameters ## Parameters
Parameters can be left unspecified if the default value is acceptable. Parameters can be left unspecified if the default value is acceptable.
@ -29,7 +21,7 @@ Parameters can be left unspecified if the default value is acceptable.
| pixels_per_unit | numeric | 100 | After the scaling factor is applied, this many pixels will equal one unit of measurement, where the units are indicated by `resize_units`. This is only used when resizing is done after the trial ends (i.e. the `resize_units` parameter is not "none"). | | pixels_per_unit | numeric | 100 | After the scaling factor is applied, this many pixels will equal one unit of measurement, where the units are indicated by `resize_units`. This is only used when resizing is done after the trial ends (i.e. the `resize_units` parameter is not "none"). |
| adjustment_prompt | HTML string | "Click and drag the lower right corner of the image until it is the same size as a credit card held up to the screen. You can use any card that is the same size as a credit card, like a membership card or driver's license. If you do not have access to a real card you can use a ruler to measure the image width to 3.37 inches or 85.6 mm." | This string can contain HTML markup. Any content here will be displayed **below the card stimulus** during the resizing phase. | | adjustment_prompt | HTML string | "Click and drag the lower right corner of the image until it is the same size as a credit card held up to the screen. You can use any card that is the same size as a credit card, like a membership card or driver's license. If you do not have access to a real card you can use a ruler to measure the image width to 3.37 inches or 85.6 mm." | This string can contain HTML markup. Any content here will be displayed **below the card stimulus** during the resizing phase. |
| adjustment_button_prompt | HTML string | "Click here when the image is the correct size" | Content of the button displayed below the card stimulus during the resizing phase. | | adjustment_button_prompt | HTML string | "Click here when the image is the correct size" | Content of the button displayed below the card stimulus during the resizing phase. |
| item_path | string | "img/card.png" | Path of the item to be presented in the card stimulus during the resizing phase. _The default image is available in `/examples/img/card.png`_ | | item_path | string | null | Path of the item to be presented in the card stimulus during the resizing phase. If `null` then no image is shown, and a solid color background is used instead. _An example image is available in `/examples/img/card.png`_ |
| item_height_mm | numeric | 53.98 | The known height of the physical item (e.g. credit card) to be measured, in mm. | | item_height_mm | numeric | 53.98 | The known height of the physical item (e.g. credit card) to be measured, in mm. |
| item_width_mm | numeric | 85.6 | The known width of the physical item (e.g. credit card) to be measured, in mm. | | item_width_mm | numeric | 85.6 | The known width of the physical item (e.g. credit card) to be measured, in mm. |
| item_init_size | numeric | 250 | The initial size of the card stimulus, in pixels, along its largest dimension. | | item_init_size | numeric | 250 | The initial size of the card stimulus, in pixels, along its largest dimension. |

View File

@ -25,7 +25,8 @@
type: jsPsychVirtualChinrest, type: jsPsychVirtualChinrest,
blindspot_reps: 2, blindspot_reps: 2,
resize_units: "cm", resize_units: "cm",
pixels_per_unit: 50 pixels_per_unit: 50,
item_path: 'img/card.png',
}; };
// one blindspot estimate // one blindspot estimate
@ -45,7 +46,7 @@
// note: pixels_per_unit will be ignored // note: pixels_per_unit will be ignored
let no_resize = { let no_resize = {
type: jsPsychVirtualChinrest, type: jsPsychVirtualChinrest,
blindspot_reps: 1, blindspot_reps: 3,
resize_units: "none", resize_units: "none",
pixels_per_unit: 50 pixels_per_unit: 50
}; };
@ -65,7 +66,7 @@
prompt: '<p>The stimulus above should be 4cm x 4cm if resizing worked properly.</p>' prompt: '<p>The stimulus above should be 4cm x 4cm if resizing worked properly.</p>'
}; };
jsPsych.run([no_resize, validation_trial]); // deg_resize, no_resize, error_trial jsPsych.run([cm_resize, validation_trial]); // deg_resize, no_resize, error_trial
</script> </script>
</html> </html>

290
package-lock.json generated
View File

@ -15,7 +15,7 @@
"lint-staged": "^12.3.5", "lint-staged": "^12.3.5",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"turbo": "^1.1.6" "turbo": "^1.1.10"
}, },
"engines": { "engines": {
"node": ">=14.0.0", "node": ">=14.0.0",
@ -14115,40 +14115,185 @@
} }
}, },
"node_modules/turbo": { "node_modules/turbo": {
"version": "1.1.6", "version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.1.10.tgz",
"integrity": "sha512-y8vx8uIyBRFI3aFjZ3PeGaOvYtNk6t7xNLzRsPY+xtnknTeqdBad56ElS8z+j0RyVwKCvI+wgvTHGkEle4VnJA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MPL-2.0",
"bin": { "bin": {
"turbo": "bin/turbo" "turbo": "bin/turbo"
}, },
"optionalDependencies": { "optionalDependencies": {
"turbo-darwin-64": "1.1.6", "turbo-darwin-64": "1.1.10",
"turbo-darwin-arm64": "1.1.6", "turbo-darwin-arm64": "1.1.10",
"turbo-freebsd-64": "1.1.6", "turbo-freebsd-64": "1.1.10",
"turbo-freebsd-arm64": "1.1.6", "turbo-freebsd-arm64": "1.1.10",
"turbo-linux-32": "1.1.6", "turbo-linux-32": "1.1.10",
"turbo-linux-64": "1.1.6", "turbo-linux-64": "1.1.10",
"turbo-linux-arm": "1.1.6", "turbo-linux-arm": "1.1.10",
"turbo-linux-arm64": "1.1.6", "turbo-linux-arm64": "1.1.10",
"turbo-linux-mips64le": "1.1.6", "turbo-linux-mips64le": "1.1.10",
"turbo-linux-ppc64le": "1.1.6", "turbo-linux-ppc64le": "1.1.10",
"turbo-windows-32": "1.1.6", "turbo-windows-32": "1.1.10",
"turbo-windows-64": "1.1.6" "turbo-windows-64": "1.1.10"
} }
}, },
"node_modules/turbo-linux-64": { "node_modules/turbo-darwin-64": {
"version": "1.1.6", "version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.1.10.tgz",
"integrity": "sha512-MY/1mHg+tS/GaZKG805e5JSGNS8A4j/M2GzLwCbNL+lwGMfneNASri1vAd80ss3T2MgMsfsFMVyIQJljqpDBvA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true, "dev": true,
"license": "MPL-2.0", "optional": true,
"os": [
"darwin"
]
},
"node_modules/turbo-darwin-arm64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.1.10.tgz",
"integrity": "sha512-gMPLseYqGKwdy6UHVWKMLA433ZTfQRV5FlYz5n4XVtx30cF6ajOqq12ykeCUUX/lZkH4Uq5zT0tNEYpUhUw7mA==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/turbo-freebsd-64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-freebsd-64/-/turbo-freebsd-64-1.1.10.tgz",
"integrity": "sha512-wra27mvakr5ZFceQnCCSR8gHQtKV8Q0EhtzO/wEdyhEssw0wVaNtMHUOOdvFN0HLmjQmmLZgmfZbURc83UDuZQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/turbo-freebsd-arm64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.1.10.tgz",
"integrity": "sha512-J2I76pTwtrEVjHt1+zWY/s/Y0YIGdWHBIWOjhCXi1E8dav98oGw+WUaiFwzAkcksAblOhNpDL3qhnrnm7kHqrg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/turbo-linux-32": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-32/-/turbo-linux-32-1.1.10.tgz",
"integrity": "sha512-d1ILhEv2B/lOtpH4niFUKGb8YMU6G7gNCQCY6wG+SXARWJtDti+KiNWESechD5DycCIMgtE40XNy/c1US+LI5g==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/turbo-linux-64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.1.10.tgz",
"integrity": "sha512-8VEOiNJFNfUMZOyrN32wOcdT1Ik1nlIuTwkO4UeonAJhuWjTvdDLPCQkz0SECTu60q90l6nXCnNYtoZA6LrZzA==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-linux-arm": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-arm/-/turbo-linux-arm-1.1.10.tgz",
"integrity": "sha512-qJ50K/s5MjpHjam+UdnK3GniEIv5XOBCZOGslgMMyz8V/q43vhB9BU9HQODclM89uQgsKxhs8Fue6ytOY4vIpg==",
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-linux-arm64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.1.10.tgz",
"integrity": "sha512-ng3dEEL4SbBudF/UZzsOrfyJh8DLtTHawTepeS30FdtvYuVBXdCPc5BAhbawGoau/2AV4vrN3qzh9e3LCqD6Qg==",
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-linux-mips64le": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-mips64le/-/turbo-linux-mips64le-1.1.10.tgz",
"integrity": "sha512-Jd4yH7ZEXCo0xmdJWZ6YsyqcNLyL5vRU3j5ZT+1W97YJCT+g+1on3/nd3rBVPzVz52lb8JIqgGtrBrnOO0AWJg==",
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-linux-ppc64le": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.1.10.tgz",
"integrity": "sha512-YF8+Oi53glqY29O1A7KJsHZxBzeVBobYFnPEXMt8vm+ouuo8kkbxXxShOP4h+33YGEkesTw/CTXtfDC1Xj1hDw==",
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/turbo-windows-32": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-windows-32/-/turbo-windows-32-1.1.10.tgz",
"integrity": "sha512-IO92tVTCtWVPPgcCjf8J7AmBEcwnjv1zPq7t9GFdqZ/6QA06atgPJNzQ/QvyzbzJgUsJUN2ByzwT04o4QUbrBQ==",
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/turbo-windows-64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.1.10.tgz",
"integrity": "sha512-g/RIXaVDaOgliHEJuOsuB6Tefwue9fXBH1/iIH9dmT3Z7lL0banGh+C10RW6Jd6PBPMoPBWir9PLYuzxoPcCNQ==",
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/type": { "node_modules/type": {
"version": "1.2.0", "version": "1.2.0",
"license": "ISC" "license": "ISC"
@ -25552,25 +25697,106 @@
} }
}, },
"turbo": { "turbo": {
"version": "1.1.6", "version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo/-/turbo-1.1.10.tgz",
"integrity": "sha512-y8vx8uIyBRFI3aFjZ3PeGaOvYtNk6t7xNLzRsPY+xtnknTeqdBad56ElS8z+j0RyVwKCvI+wgvTHGkEle4VnJA==",
"dev": true, "dev": true,
"requires": { "requires": {
"turbo-darwin-64": "1.1.6", "turbo-darwin-64": "1.1.10",
"turbo-darwin-arm64": "1.1.6", "turbo-darwin-arm64": "1.1.10",
"turbo-freebsd-64": "1.1.6", "turbo-freebsd-64": "1.1.10",
"turbo-freebsd-arm64": "1.1.6", "turbo-freebsd-arm64": "1.1.10",
"turbo-linux-32": "1.1.6", "turbo-linux-32": "1.1.10",
"turbo-linux-64": "1.1.6", "turbo-linux-64": "1.1.10",
"turbo-linux-arm": "1.1.6", "turbo-linux-arm": "1.1.10",
"turbo-linux-arm64": "1.1.6", "turbo-linux-arm64": "1.1.10",
"turbo-linux-mips64le": "1.1.6", "turbo-linux-mips64le": "1.1.10",
"turbo-linux-ppc64le": "1.1.6", "turbo-linux-ppc64le": "1.1.10",
"turbo-windows-32": "1.1.6", "turbo-windows-32": "1.1.10",
"turbo-windows-64": "1.1.6" "turbo-windows-64": "1.1.10"
} }
}, },
"turbo-darwin-64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.1.10.tgz",
"integrity": "sha512-MY/1mHg+tS/GaZKG805e5JSGNS8A4j/M2GzLwCbNL+lwGMfneNASri1vAd80ss3T2MgMsfsFMVyIQJljqpDBvA==",
"dev": true,
"optional": true
},
"turbo-darwin-arm64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.1.10.tgz",
"integrity": "sha512-gMPLseYqGKwdy6UHVWKMLA433ZTfQRV5FlYz5n4XVtx30cF6ajOqq12ykeCUUX/lZkH4Uq5zT0tNEYpUhUw7mA==",
"dev": true,
"optional": true
},
"turbo-freebsd-64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-freebsd-64/-/turbo-freebsd-64-1.1.10.tgz",
"integrity": "sha512-wra27mvakr5ZFceQnCCSR8gHQtKV8Q0EhtzO/wEdyhEssw0wVaNtMHUOOdvFN0HLmjQmmLZgmfZbURc83UDuZQ==",
"dev": true,
"optional": true
},
"turbo-freebsd-arm64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-freebsd-arm64/-/turbo-freebsd-arm64-1.1.10.tgz",
"integrity": "sha512-J2I76pTwtrEVjHt1+zWY/s/Y0YIGdWHBIWOjhCXi1E8dav98oGw+WUaiFwzAkcksAblOhNpDL3qhnrnm7kHqrg==",
"dev": true,
"optional": true
},
"turbo-linux-32": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-32/-/turbo-linux-32-1.1.10.tgz",
"integrity": "sha512-d1ILhEv2B/lOtpH4niFUKGb8YMU6G7gNCQCY6wG+SXARWJtDti+KiNWESechD5DycCIMgtE40XNy/c1US+LI5g==",
"dev": true,
"optional": true
},
"turbo-linux-64": { "turbo-linux-64": {
"version": "1.1.6", "version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.1.10.tgz",
"integrity": "sha512-8VEOiNJFNfUMZOyrN32wOcdT1Ik1nlIuTwkO4UeonAJhuWjTvdDLPCQkz0SECTu60q90l6nXCnNYtoZA6LrZzA==",
"dev": true,
"optional": true
},
"turbo-linux-arm": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-arm/-/turbo-linux-arm-1.1.10.tgz",
"integrity": "sha512-qJ50K/s5MjpHjam+UdnK3GniEIv5XOBCZOGslgMMyz8V/q43vhB9BU9HQODclM89uQgsKxhs8Fue6ytOY4vIpg==",
"dev": true,
"optional": true
},
"turbo-linux-arm64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.1.10.tgz",
"integrity": "sha512-ng3dEEL4SbBudF/UZzsOrfyJh8DLtTHawTepeS30FdtvYuVBXdCPc5BAhbawGoau/2AV4vrN3qzh9e3LCqD6Qg==",
"dev": true,
"optional": true
},
"turbo-linux-mips64le": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-mips64le/-/turbo-linux-mips64le-1.1.10.tgz",
"integrity": "sha512-Jd4yH7ZEXCo0xmdJWZ6YsyqcNLyL5vRU3j5ZT+1W97YJCT+g+1on3/nd3rBVPzVz52lb8JIqgGtrBrnOO0AWJg==",
"dev": true,
"optional": true
},
"turbo-linux-ppc64le": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-linux-ppc64le/-/turbo-linux-ppc64le-1.1.10.tgz",
"integrity": "sha512-YF8+Oi53glqY29O1A7KJsHZxBzeVBobYFnPEXMt8vm+ouuo8kkbxXxShOP4h+33YGEkesTw/CTXtfDC1Xj1hDw==",
"dev": true,
"optional": true
},
"turbo-windows-32": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-windows-32/-/turbo-windows-32-1.1.10.tgz",
"integrity": "sha512-IO92tVTCtWVPPgcCjf8J7AmBEcwnjv1zPq7t9GFdqZ/6QA06atgPJNzQ/QvyzbzJgUsJUN2ByzwT04o4QUbrBQ==",
"dev": true,
"optional": true
},
"turbo-windows-64": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.1.10.tgz",
"integrity": "sha512-g/RIXaVDaOgliHEJuOsuB6Tefwue9fXBH1/iIH9dmT3Z7lL0banGh+C10RW6Jd6PBPMoPBWir9PLYuzxoPcCNQ==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },

View File

@ -29,7 +29,7 @@
"lint-staged": "^12.3.5", "lint-staged": "^12.3.5",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"prettier-plugin-import-sort": "^0.0.7", "prettier-plugin-import-sort": "^0.0.7",
"turbo": "^1.1.6" "turbo": "^1.1.10"
}, },
"prettier": { "prettier": {
"printWidth": 100 "printWidth": 100

View File

@ -42,8 +42,7 @@ const info = <const>{
item_path: { item_path: {
type: ParameterType.IMAGE, type: ParameterType.IMAGE,
pretty_name: "Item path", pretty_name: "Item path",
default: "img/card.png", default: null,
// TO DO: I think the background image should be optional, in which case we don't want to try to auto-preload this parameter?
preload: false, preload: false,
}, },
/** The height of the item to be measured, in mm. */ /** The height of the item to be measured, in mm. */
@ -147,6 +146,12 @@ declare global {
class VirtualChinrestPlugin implements JsPsychPlugin<Info> { class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
static info = info; static info = info;
private ball_size: number = 30;
private ball: HTMLElement = null;
private container: HTMLElement = null;
private reps_remaining = 0;
private ball_animation_frame_id = null;
constructor(private jsPsych: JsPsych) {} constructor(private jsPsych: JsPsych) {}
trial(display_element: HTMLElement, trial: TrialType<Info>) { trial(display_element: HTMLElement, trial: TrialType<Info>) {
@ -161,6 +166,8 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
return; return;
} }
this.reps_remaining = trial.blindspot_reps;
/** some additional parameter configuration */ /** some additional parameter configuration */
let trial_data = <any>{ let trial_data = <any>{
item_width_mm: trial.item_width_mm, item_width_mm: trial.item_width_mm,
@ -183,7 +190,11 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
/** create content for first screen, resizing card */ /** create content for first screen, resizing card */
let pagesize_content = ` let pagesize_content = `
<div id="page-size"> <div id="page-size">
<div id="item" style="border: none; height: ${start_div_height}px; width: ${start_div_width}px; margin: 5px auto; background-color: none; position: relative; background-image: url(${trial.item_path}); background-size: 100% auto; background-repeat: no-repeat;"> <div id="item" style="border: none; height: ${start_div_height}px; width: ${start_div_width}px; margin: 5px auto; background-color: #ddd; position: relative; ${
trial.item_path === null
? ""
: `background-image: url(${trial.item_path}); background-size: 100% auto; background-repeat: no-repeat;`
}">
<div id="jspsych-resize-handle" style="cursor: nwse-resize; background-color: none; width: ${adjust_size}px; height: ${adjust_size}px; border: 5px solid red; border-left: 0; border-top: 0; position: absolute; bottom: 0; right: 0;"> <div id="jspsych-resize-handle" style="cursor: nwse-resize; background-color: none; width: ${adjust_size}px; height: ${adjust_size}px; border: 5px solid red; border-left: 0; border-top: 0; position: absolute; bottom: 0; right: 0;">
</div> </div>
</div> </div>
@ -198,7 +209,7 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
let blindspot_content = ` let blindspot_content = `
<div id="blind-spot"> <div id="blind-spot">
${trial.blindspot_prompt} ${trial.blindspot_prompt}
<div id="svgDiv" style="width:1000px;height:200px;"></div> <div id="svgDiv" style="height:100px; position:relative;"></div>
<button class="btn btn-primary" id="proceed" style="display:none;"> + <button class="btn btn-primary" id="proceed" style="display:none;"> +
${trial.blindspot_done_prompt} + ${trial.blindspot_done_prompt} +
</button> </button>
@ -220,6 +231,7 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
display_element.innerHTML = `<div id="content" style="width: 900px; margin: 0 auto;"></div>`; display_element.innerHTML = `<div id="content" style="width: 900px; margin: 0 auto;"></div>`;
const start_time = performance.now(); const start_time = performance.now();
startResizePhase(); startResizePhase();
function startResizePhase() { function startResizePhase() {
@ -271,7 +283,7 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
function finishResizePhase() { function finishResizePhase() {
// add item width info to data // add item width info to data
const item_width_px = getScaledItemWidth(); const item_width_px = document.querySelector("#item").getBoundingClientRect().width;
trial_data["item_width_px"] = Math.round(item_width_px); trial_data["item_width_px"] = Math.round(item_width_px);
const px2mm = convertPixelsToMM(item_width_px); const px2mm = convertPixelsToMM(item_width_px);
trial_data["px2mm"] = accurateRound(px2mm, 2); trial_data["px2mm"] = accurateRound(px2mm, 2);
@ -290,9 +302,21 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
slider_clck: false, slider_clck: false,
}; };
// add the content to the page // add the content to the page
document.querySelector("#content").innerHTML = blindspot_content; display_element.querySelector("#content").innerHTML = blindspot_content;
this.container = display_element.querySelector("#svgDiv");
// draw the ball and fixation square // draw the ball and fixation square
drawBall(); drawBall();
resetAndWaitForBallStart();
};
const resetAndWaitForBallStart = () => {
const rectX = this.container.getBoundingClientRect().width - this.ball_size;
const ballX = rectX * 0.85; // define where the ball is
this.ball.style.left = `${ballX}px`;
// wait for a spacebar to begin the animations // wait for a spacebar to begin the animations
this.jsPsych.pluginAPI.getKeyboardResponse({ this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: startBall, callback_function: startBall,
@ -304,20 +328,33 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
}; };
const startBall = () => { const startBall = () => {
const ball_position_listener = this.jsPsych.pluginAPI.getKeyboardResponse({ this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: recordPosition, callback_function: recordPosition,
valid_responses: [" "], valid_responses: [" "],
rt_method: "performance", rt_method: "performance",
allow_held_key: false, allow_held_key: false,
persist: true, persist: false,
}); });
animateBall();
this.ball_animation_frame_id = requestAnimationFrame(animateBall);
}; };
const finishBlindSpotPhase = () => { const finishBlindSpotPhase = () => {
window.ball.stop(); const angle = 13.5;
this.jsPsych.pluginAPI.cancelAllKeyboardResponses(); // calculate average ball position
const sum = blindspot_config_data["ball_pos"].reduce((a, b) => a + b, 0);
const ballPosLen = blindspot_config_data["ball_pos"].length;
blindspot_config_data["avg_ball_pos"] = accurateRound(sum / ballPosLen, 2);
// calculate distance between avg ball position and square
const ball_sqr_distance =
(blindspot_config_data["square_pos"] - blindspot_config_data["avg_ball_pos"]) /
trial_data["px2mm"];
// calculate viewing distance in mm
const viewDistance = ball_sqr_distance / Math.tan(deg_to_radians(angle));
trial_data["view_dist_mm"] = accurateRound(viewDistance, 2);
if (trial.viewing_distance_report == "none") { if (trial.viewing_distance_report == "none") {
endTrial(); endTrial();
@ -400,68 +437,53 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
this.jsPsych.finishTrial(trial_data); this.jsPsych.finishTrial(trial_data);
}; };
function getScaledItemWidth() { const drawBall = () => {
return document.querySelector("#item").getBoundingClientRect().width; this.container.innerHTML = `
} <div id="virtual-chinrest-circle" style="position: absolute; background-color: #f00; width: ${this.ball_size}px; height: ${this.ball_size}px; border-radius:${this.ball_size}px;"></div>
<div id="virtual-chinrest-square" style="position: absolute; background-color: #000; width: ${this.ball_size}px; height: ${this.ball_size}px;"></div>
`;
function drawBall(pos = 180) { const ball: HTMLElement = this.container.querySelector("#virtual-chinrest-circle");
// pos: define where the fixation square should be. const square: HTMLElement = this.container.querySelector("#virtual-chinrest-square");
// @ts-expect-error
var mySVG = SVG("svgDiv");
const rectX = trial_data["px2mm"] * pos;
const ballX = rectX * 0.6; // define where the ball is
var ball = mySVG.circle(30).move(ballX, 50).fill("#f00");
window.ball = ball;
var square = mySVG.rect(30, 30).move(Math.min(rectX - 50, 950), 50); //square position
blindspot_config_data["square_pos"] = accurateRound(square.cx(), 2);
blindspot_config_data["rectX"] = rectX;
blindspot_config_data["ballX"] = ballX;
}
function animateBall() { const rectX = this.container.getBoundingClientRect().width - this.ball_size;
window.ball const ballX = rectX * 0.85; // define where the ball is
.animate(7000)
.during((pos) => {
let moveX = -pos * blindspot_config_data["ballX"];
window.moveX = moveX;
let moveY = 0;
window.ball.attr({ transform: "translate(" + moveX + "," + moveY + ")" }); //jqueryToVanilla: el.getAttribute('');
})
.loop(true, false)
.after(() => {
animateBall();
});
}
function recordPosition() { ball.style.left = `${ballX}px`;
// angle: define horizontal blind spot entry point position in degrees. square.style.left = `${rectX}px`;
const angle = 13.5;
blindspot_config_data["ball_pos"].push(accurateRound(window.ball.cx() + window.moveX, 2)); this.ball = ball;
var sum = blindspot_config_data["ball_pos"].reduce((a, b) => a + b, 0);
var ballPosLen = blindspot_config_data["ball_pos"].length; blindspot_config_data["square_pos"] = accurateRound(getElementCenter(square).x, 2);
blindspot_config_data["avg_ball_pos"] = accurateRound(sum / ballPosLen, 2); };
var ball_sqr_distance =
(blindspot_config_data["square_pos"] - blindspot_config_data["avg_ball_pos"]) / const animateBall = () => {
trial_data["px2mm"]; const dx = -2;
var viewDistance = ball_sqr_distance / Math.tan(deg_to_radians(angle)); const x = parseInt(this.ball.style.left);
trial_data["view_dist_mm"] = accurateRound(viewDistance, 2); this.ball.style.left = `${x + dx}px`;
this.ball_animation_frame_id = requestAnimationFrame(animateBall);
};
const recordPosition = () => {
cancelAnimationFrame(this.ball_animation_frame_id);
blindspot_config_data["ball_pos"].push(accurateRound(getElementCenter(this.ball).x, 2));
//counter and stop //counter and stop
var counter = Number(document.querySelector("#click").textContent); this.reps_remaining--;
counter = counter - 1;
(document.querySelector("#click") as HTMLDivElement).textContent = Math.max( (document.querySelector("#click") as HTMLDivElement).textContent = Math.max(
counter, this.reps_remaining,
0 0
).toString(); ).toString();
if (counter <= 0) {
if (this.reps_remaining <= 0) {
finishBlindSpotPhase(); finishBlindSpotPhase();
return;
} else { } else {
window.ball.stop(); resetAndWaitForBallStart();
animateBall();
}
} }
};
function convertPixelsToMM(item_width_px) { function convertPixelsToMM(item_width_px) {
const px2mm = item_width_px / trial_data["item_width_mm"]; const px2mm = item_width_px / trial_data["item_width_mm"];
@ -472,6 +494,14 @@ class VirtualChinrestPlugin implements JsPsychPlugin<Info> {
return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals); return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals);
} }
function getElementCenter(el: HTMLElement) {
const box = el.getBoundingClientRect();
return {
x: box.left + box.width / 2,
y: box.top + box.height / 2,
};
}
//helper function for radians //helper function for radians
// Converts from degrees to radians. // Converts from degrees to radians.
const deg_to_radians = (degrees: number) => { const deg_to_radians = (degrees: number) => {