Psychophysics is the quantitative study of the relationship between physical stimuli and the sensations they produce. The name and the programme come from Gustav Fechner, whose Elemente der Psychophysik (1860) proposed that sensation could be measured indirectly by finding the smallest stimulus differences a person can detect. Fechner built on Ernst Weber's observation that the just-noticeable difference (JND) between two stimuli is roughly a constant fraction of their magnitude (Weber's law, ΔI / I = k). From it he derived Fechner's law, which states that sensation grows with the logarithm of stimulus intensity.
A century later, S. S. Stevens (1957) argued that people can directly rate sensation magnitude, and that the resulting data fit a power function, S = k·Ia, rather than a logarithm. The exponent a differs by modality: below 1 for brightness and loudness (compressive), close to 1 for visual length, above 1 for electric shock (expansive). Weber, Fechner and Stevens remain the starting point of any psychophysics course, and the methods they inspired are still in use today.
The goal of most psychophysical procedures is to estimate a threshold: the stimulus level at which a participant detects the stimulus (absolute threshold) or tells two stimuli apart (difference threshold) with a given probability, typically 50–75 % correct.
Laboratory psychophysics relies on calibrated hardware. A web browser on a participant's own device removes most of that control, so it is worth being explicit about what transfers and what does not.
requestAnimationFrame, so durations are quantised to frames and a "50 ms" stimulus will actually last 3 or 4 frames. Paradigms that need single-frame precision, such as very brief masked presentations, are risky online; anything at 100 ms or longer is generally fine.jsPsychVirtualChinrest plugin (Li, Joo, Yeatman & Binda, 2020) solves a large part of this: participants resize an on-screen rectangle to match a credit card, which gives pixels per millimetre, and then fixate a point while a dot moves until it disappears into their blind spot, which gives viewing distance. The plugin reports both values so you can scale stimuli in degrees for the rest of the session.The snippet below presents a grey disc on a grey background and asks whether it appeared on the left or the right. Contrast goes down after two correct answers in a row and up after one error. The loop stops after eight reversals, and the threshold is the mean contrast at the last six reversals.
const jsPsych = initJsPsych();
let contrast = 0.5; // starting Michelson contrast (0..1)
let step = 0.05;
let correctStreak = 0;
let lastDirection = null;
let reversals = [];
const stim = (side, c) => {
const lum = Math.min(255, Math.round(128 + 127 * c));
const disc = `<div style="width:60px;height:60px;border-radius:50%;
background:rgb(${lum},${lum},${lum});display:inline-block"></div>`;
const blank = `<div style="width:60px;height:60px;display:inline-block"></div>`;
return `<div style="background:rgb(128,128,128);padding:80px">
${side === "left" ? disc + blank : blank + disc}</div>`;
};
const trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: () => stim(jsPsych.evaluateTimelineVariable("side"), contrast),
choices: ["f", "j"],
stimulus_duration: 200,
trial_duration: 2000,
data: { task: "staircase", side: jsPsych.timelineVariable("side") },
on_start: (t) => { t.data.contrast = contrast; },
on_finish: (data) => {
const expected = data.side === "left" ? "f" : "j";
data.correct = jsPsych.pluginAPI.compareKeys(data.response, expected);
let direction = null;
if (data.correct) {
correctStreak++;
if (correctStreak === 2) { contrast -= step; correctStreak = 0; direction = "down"; }
} else {
contrast += step; correctStreak = 0; direction = "up";
}
if (direction && lastDirection && direction !== lastDirection) reversals.push(data.contrast);
if (direction) lastDirection = direction;
contrast = Math.min(1, Math.max(0.01, contrast));
}
};
const staircase = {
timeline: [trial],
timeline_variables: [{ side: "left" }, { side: "right" }],
sample: { type: "with-replacement", size: 1 },
loop_function: () => reversals.length < 8
};
const done = {
type: jsPsychHtmlKeyboardResponse,
stimulus: () => {
const last = reversals.slice(-6);
const thr = last.reduce((a, b) => a + b, 0) / last.length;
jsPsych.data.addProperties({ threshold: thr.toFixed(3) });
return `<p>Done. Estimated threshold: ${thr.toFixed(3)}</p>`;
}
};
jsPsych.run([staircase, done]);
For a real study you would add a fixation cross, practice trials, a jsPsychFullscreen trial and a jsPsychVirtualChinrest trial at the start, and you would probably step contrast in log units rather than linearly. If you need a faster-converging estimate, the QUEST algorithm can be implemented in plain JavaScript and plugged into the same on_finish structure.
Paste the code into the Cognition editor, select jsPsych 8.2.3, and use the live preview to run the staircase yourself while watching the console for errors. Upload any images you need as stimuli, then share the generated https://<token>.cognition.run link with participants. Every trial, including the contrast and correct columns added above, is sent to the server in real time, and a service worker buffers data locally if a participant's connection drops. When the study ends you download a CSV or JSON per participant and fit psychometric functions in R or Python, or use the built-in analyzer for quick comparisons between conditions.
Create a free account to host up to four tasks with sixty participants each, or read the documentation for details on stimuli, consent forms and data export.