mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 11:10:54 +00:00
implementing many features of sketchpad plugin, add example file
This commit is contained in:
parent
bddae3eef7
commit
9605c431c6
BIN
examples/img/navarro_night_tree_05_575.jpg
Normal file
BIN
examples/img/navarro_night_tree_05_575.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 111 KiB |
@ -8,12 +8,20 @@
|
||||
<body></body>
|
||||
<script>
|
||||
|
||||
var jsPsych = initJsPsych();
|
||||
var jsPsych = initJsPsych({
|
||||
on_finish: () => {
|
||||
jsPsych.data.displayData()
|
||||
}
|
||||
});
|
||||
|
||||
var sketchpad_trial = {
|
||||
type: jsPsychSketchpad,
|
||||
canvas_border_width: 2,
|
||||
canvas_shape: 'circle'
|
||||
background_image: 'img/navarro_night_tree_05_575.jpg',
|
||||
prompt: '<p>Draw something!</p>',
|
||||
stroke_color_palette: [
|
||||
'black', 'red', 'orange','yellow', 'green', 'blue', 'purple'
|
||||
]
|
||||
}
|
||||
|
||||
jsPsych.run([sketchpad_trial]);
|
||||
|
@ -25,26 +25,69 @@ const info = <const>{
|
||||
default: 500,
|
||||
},
|
||||
/**
|
||||
* Diametere of the canvas (when `canvas_shape` is `'circle'`) in pixels.
|
||||
* Diameter of the canvas (when `canvas_shape` is `'circle'`) in pixels.
|
||||
*/
|
||||
canvas_diameter: {
|
||||
type: ParameterType.INT,
|
||||
default: 500,
|
||||
},
|
||||
/**
|
||||
* Diametere of the canvas (when `canvas_shape` is `'circle'`) in pixels.
|
||||
* This width of the border around the canvas element
|
||||
*/
|
||||
canvas_border_width: {
|
||||
type: ParameterType.INT,
|
||||
default: 0,
|
||||
},
|
||||
/**
|
||||
* Diametere of the canvas (when `canvas_shape` is `'circle'`) in pixels.
|
||||
* The color of the border around the canvas element.
|
||||
*/
|
||||
canvas_border_color: {
|
||||
type: ParameterType.STRING,
|
||||
default: "#000",
|
||||
},
|
||||
/**
|
||||
* Path to an image to render as the background of the canvas.
|
||||
*/
|
||||
background_image: {
|
||||
type: ParameterType.IMAGE,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* The width of the strokes on the canvas.
|
||||
*/
|
||||
stroke_width: {
|
||||
type: ParameterType.INT,
|
||||
default: 2,
|
||||
},
|
||||
/**
|
||||
* The color of the stroke on the canvas
|
||||
*/
|
||||
stroke_color: {
|
||||
type: ParameterType.STRING,
|
||||
default: "#000000",
|
||||
},
|
||||
/**
|
||||
* An array of colors to render as a palette of options for stroke colors.
|
||||
*/
|
||||
stroke_color_palette: {
|
||||
type: ParameterType.STRING,
|
||||
array: true,
|
||||
default: [],
|
||||
},
|
||||
/**
|
||||
* HTML content to render above or below the canvas (use `prompt_location` parameter to change location).
|
||||
*/
|
||||
prompt: {
|
||||
type: ParameterType.HTML_STRING,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* Location of the `prompt` content. Can be 'abovecanvas' or 'belowcanvas' or 'belowbutton'.
|
||||
*/
|
||||
prompt_location: {
|
||||
type: ParameterType.STRING,
|
||||
default: "abovecanvas",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -60,22 +103,69 @@ type Info = typeof info;
|
||||
*/
|
||||
class SketchpadPlugin implements JsPsychPlugin<Info> {
|
||||
static info = info;
|
||||
private display: HTMLElement;
|
||||
private params: TrialType<Info>;
|
||||
private sketchpad: HTMLCanvasElement;
|
||||
private is_drawing = false;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
private trial_finished_handler;
|
||||
private background_image;
|
||||
private strokes = [];
|
||||
private stroke = [];
|
||||
private undo_history = [];
|
||||
private current_stroke_color;
|
||||
private start_time;
|
||||
|
||||
constructor(private jsPsych: JsPsych) {}
|
||||
|
||||
trial(display_element: HTMLElement, trial: TrialType<Info>) {
|
||||
trial(display_element: HTMLElement, trial: TrialType<Info>, on_load: () => void) {
|
||||
this.display = display_element;
|
||||
this.params = trial;
|
||||
|
||||
document.querySelector("head").insertAdjacentHTML(
|
||||
"beforeend",
|
||||
`<style id="sketchpad-styles">
|
||||
.sketchpad-color-select {
|
||||
cursor: pointer; height: 33px; width: 33px; border-radius: 4px; padding: 0; border: 1px solid #ccc;
|
||||
}
|
||||
</style>`
|
||||
);
|
||||
|
||||
this.current_stroke_color = trial.stroke_color;
|
||||
|
||||
this.render_display();
|
||||
|
||||
this.setup_event_listeners();
|
||||
|
||||
this.add_background_image().then(() => {
|
||||
on_load();
|
||||
});
|
||||
|
||||
this.start_time = performance.now();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.trial_finished_handler = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
private render_display() {
|
||||
let canvas_html;
|
||||
if (trial.canvas_shape == "rectangle") {
|
||||
if (this.params.canvas_shape == "rectangle") {
|
||||
canvas_html = `
|
||||
<canvas id="sketchpad-canvas" width="${trial.canvas_width}" height="${trial.canvas_height}" style="border: ${trial.canvas_border_width}px solid ${trial.canvas_border_color};"></canvas>
|
||||
<canvas id="sketchpad-canvas"
|
||||
width="${this.params.canvas_width}"
|
||||
height="${this.params.canvas_height}"
|
||||
style="border: ${this.params.canvas_border_width}px solid ${this.params.canvas_border_color};"></canvas>
|
||||
`;
|
||||
} else if (trial.canvas_shape == "circle") {
|
||||
} else if (this.params.canvas_shape == "circle") {
|
||||
canvas_html = `
|
||||
<canvas id="sketchpad-canvas" width="${trial.canvas_diameter}" height="${
|
||||
trial.canvas_diameter
|
||||
}" style="border: ${trial.canvas_border_width}px solid ${
|
||||
trial.canvas_border_color
|
||||
}; border-radius:${trial.canvas_diameter / 2}px;"></canvas>
|
||||
<canvas id="sketchpad-canvas"
|
||||
width="${this.params.canvas_diameter}"
|
||||
height="${this.params.canvas_diameter}"
|
||||
style="border: ${this.params.canvas_border_width}px solid ${
|
||||
this.params.canvas_border_color
|
||||
}; border-radius:${this.params.canvas_diameter / 2}px;">
|
||||
</canvas>
|
||||
`;
|
||||
} else {
|
||||
throw new Error(
|
||||
@ -83,21 +173,189 @@ class SketchpadPlugin implements JsPsychPlugin<Info> {
|
||||
);
|
||||
}
|
||||
|
||||
let sketchpad_controls = `<div id="sketchpad-controls" style="line-height: 1; width:${
|
||||
this.params.canvas_shape == "rectangle"
|
||||
? this.params.canvas_width
|
||||
: this.params.canvas_diameter
|
||||
}px;">`;
|
||||
|
||||
sketchpad_controls += `<div id="sketchpad-color-palette" style="width:50%; display: inline-block; text-align:left;">`;
|
||||
for (const color of this.params.stroke_color_palette) {
|
||||
sketchpad_controls += `<button class="sketchpad-color-select" data-color="${color}" style="background-color:${color};"></button>`;
|
||||
}
|
||||
sketchpad_controls += `</div>`;
|
||||
|
||||
sketchpad_controls += `<div id="sketchpad-actions" style="width:50%; display:inline-block; right: 0; text-align:right;">
|
||||
<button class="jspsych-btn" id="sketchpad-clear">Clear</button>
|
||||
<button class="jspsych-btn" id="sketchpad-undo" disabled>Undo</button>
|
||||
<button class="jspsych-btn" id="sketchpad-redo" disabled>Redo</button>
|
||||
</div>`;
|
||||
|
||||
canvas_html += sketchpad_controls;
|
||||
|
||||
const finish_button_html = `<p><button class="jspsych-btn" id="sketchpad-end">Finished</button></p>`;
|
||||
|
||||
const display_html = canvas_html + finish_button_html;
|
||||
let display_html;
|
||||
if (this.params.prompt !== null) {
|
||||
if (this.params.prompt_location == "abovecanvas") {
|
||||
display_html = this.params.prompt + canvas_html + finish_button_html;
|
||||
}
|
||||
if (this.params.prompt_location == "belowcanvas") {
|
||||
display_html = canvas_html + this.params.prompt + finish_button_html;
|
||||
}
|
||||
if (this.params.prompt_location == "belowbutton") {
|
||||
display_html = canvas_html + finish_button_html + this.params.prompt;
|
||||
}
|
||||
}
|
||||
|
||||
display_element.innerHTML = display_html;
|
||||
this.display.innerHTML = display_html;
|
||||
|
||||
display_element.querySelector("#sketchpad-end").addEventListener("click", () => {
|
||||
end_trial();
|
||||
this.sketchpad = this.display.querySelector("#sketchpad-canvas");
|
||||
this.ctx = this.sketchpad.getContext("2d");
|
||||
}
|
||||
|
||||
private setup_event_listeners() {
|
||||
this.display.querySelector("#sketchpad-end").addEventListener("click", this.end_trial);
|
||||
|
||||
this.sketchpad.addEventListener("mousedown", this.start_draw);
|
||||
this.sketchpad.addEventListener("mousemove", this.move_draw);
|
||||
this.sketchpad.addEventListener("mouseup", this.end_draw);
|
||||
this.sketchpad.addEventListener("mouseleave", this.end_draw);
|
||||
|
||||
this.display.querySelector("#sketchpad-undo").addEventListener("click", this.undo);
|
||||
this.display.querySelector("#sketchpad-redo").addEventListener("click", this.redo);
|
||||
this.display.querySelector("#sketchpad-clear").addEventListener("click", this.clear);
|
||||
|
||||
const color_btns = Array.from(this.display.querySelectorAll(".sketchpad-color-select"));
|
||||
for (const btn of color_btns) {
|
||||
btn.addEventListener("click", (e) => {
|
||||
const target = e.target as HTMLButtonElement;
|
||||
this.current_stroke_color = target.getAttribute("data-color");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private add_background_image() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.params.background_image !== null) {
|
||||
this.background_image = new Image();
|
||||
this.background_image.src = this.params.background_image;
|
||||
this.background_image.onload = () => {
|
||||
this.ctx.drawImage(this.background_image, 0, 0);
|
||||
resolve(true);
|
||||
};
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private start_draw(e) {
|
||||
this.is_drawing = true;
|
||||
|
||||
const x = e.clientX - this.sketchpad.getBoundingClientRect().left;
|
||||
const y = e.clientY - this.sketchpad.getBoundingClientRect().top;
|
||||
|
||||
this.undo_history = [];
|
||||
(this.display.querySelector("#sketchpad-redo") as HTMLButtonElement).disabled = true;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(x, y);
|
||||
this.ctx.strokeStyle = this.current_stroke_color;
|
||||
this.ctx.lineJoin = "round";
|
||||
this.ctx.lineWidth = this.params.stroke_width;
|
||||
this.stroke = [];
|
||||
this.stroke.push({
|
||||
x: x,
|
||||
y: y,
|
||||
color: this.current_stroke_color,
|
||||
action: "start",
|
||||
t: Math.round(performance.now() - this.start_time),
|
||||
});
|
||||
}
|
||||
|
||||
private move_draw(e) {
|
||||
if (this.is_drawing) {
|
||||
const x = e.clientX - this.sketchpad.getBoundingClientRect().left;
|
||||
const y = e.clientY - this.sketchpad.getBoundingClientRect().top;
|
||||
|
||||
this.ctx.lineTo(x, y);
|
||||
this.ctx.stroke();
|
||||
this.stroke.push({
|
||||
x: x,
|
||||
y: y,
|
||||
action: "move",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private end_draw(e) {
|
||||
if (this.is_drawing) {
|
||||
this.stroke.push({
|
||||
action: "end",
|
||||
t: Math.round(performance.now() - this.start_time),
|
||||
});
|
||||
this.strokes.push(this.stroke);
|
||||
(this.display.querySelector("#sketchpad-undo") as HTMLButtonElement).disabled = false;
|
||||
}
|
||||
this.is_drawing = false;
|
||||
}
|
||||
|
||||
private render_drawing() {
|
||||
this.ctx.clearRect(0, 0, this.sketchpad.width, this.sketchpad.height);
|
||||
this.ctx.drawImage(this.background_image, 0, 0);
|
||||
for (const stroke of this.strokes) {
|
||||
for (const m of stroke) {
|
||||
if (m.action == "start") {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(m.x, m.y);
|
||||
this.ctx.strokeStyle = m.color;
|
||||
this.ctx.lineJoin = "round";
|
||||
this.ctx.lineWidth = this.params.stroke_width;
|
||||
}
|
||||
if (m.action == "move") {
|
||||
this.ctx.lineTo(m.x, m.y);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private undo() {
|
||||
this.undo_history.push(this.strokes.pop());
|
||||
(this.display.querySelector("#sketchpad-redo") as HTMLButtonElement).disabled = false;
|
||||
if (this.strokes.length == 0) {
|
||||
(this.display.querySelector("#sketchpad-undo") as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
this.render_drawing();
|
||||
}
|
||||
|
||||
private redo() {
|
||||
this.strokes.push(this.undo_history.pop());
|
||||
(this.display.querySelector("#sketchpad-undo") as HTMLButtonElement).disabled = false;
|
||||
if (this.undo_history.length == 0) {
|
||||
(this.display.querySelector("#sketchpad-redo") as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
this.render_drawing();
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.strokes = [];
|
||||
this.undo_history = [];
|
||||
this.render_drawing();
|
||||
(this.display.querySelector("#sketchpad-redo") as HTMLButtonElement).disabled = true;
|
||||
(this.display.querySelector("#sketchpad-undo") as HTMLButtonElement).disabled = true;
|
||||
}
|
||||
|
||||
private end_trial() {
|
||||
this.display.innerHTML = "";
|
||||
|
||||
this.jsPsych.finishTrial({
|
||||
strokes: this.strokes,
|
||||
rt: Math.round(performance.now() - this.start_time),
|
||||
});
|
||||
|
||||
const end_trial = () => {
|
||||
display_element.innerHTML = "";
|
||||
|
||||
this.jsPsych.finishTrial({});
|
||||
};
|
||||
this.trial_finished_handler();
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user