Skip to main content

@saccadejs/extension

Register it in initJsPsych to make eye tracking available to the experiment; attach it to a trial to record a gaze sample for every camera frame of that trial. Every plugin on this site requires it.

Package@saccadejs/extension
Browser globaljsPsychExtensionSaccade
Name"saccade", so it is reached as jsPsych.extensions.saccade

initialize parameters

ParameterTypeDefaultDescription
round_predictionsbooleantrueRound x and y to whole pixels in the recorded data.
auto_initializebooleanfalsePrompt for the camera and download the model during initJsPsych instead of at the saccade-preview trial.
ttanumber5Frames whose embeddings are averaged before predicting. Higher is smoother and laggier; set 1 for gaze-contingent designs.
assetsSaccadeAssets{}Model and runtime URLs. See Hosting the assets.
trackerSaccadeTrackerUse a pre-built tracker instead of constructing one. The extension will not dispose a tracker it did not create.

Trial parameters

ParameterTypeDefaultDescription
targetsstring[][]CSS selectors. Each element's bounding rectangle is recorded in saccade_targets.

Data

FieldTypeDescription
saccade_data{x, y, t}[]One row per camera frame in which a face was found and a calibrated prediction was available. x and y are viewport pixels; t is ms since the trial started.
saccade_targets{ [selector]: {x, y, width, height, top, bottom, left, right} }Bounding rectangle of each targets element, in viewport pixels.
saccade_timing{offset_ms, corrected, clock, dropped_frames, fps, tta}What timing correction was applied, and camera health. See Timing and synchrony.

t is (time.meanCapture ?? time.capture) - trialStart - (offset ?? 0): the camera's own capture stamp, averaged over the tta frames the estimate came from, with the measured lag subtracted when saccade-time-sync has run.

Methods

Reached as jsPsych.extensions.saccade.

MethodReturnsDescription
start()Promise<void>Prompt for the camera, load the model, start the frame loop. Idempotent.
pause()voidStop the frame loop; the camera stays open.
resume()voidRestart it.
isInitialized()booleanWhether start() has completed successfully.
faceDetected()booleanWhether the most recent frame contained a face.
getBackend()"webgpu" | "wasm" | nullThe execution provider the model is running on.
getTracker()SaccadeTrackerThe underlying tracker.
showVideo() / hideVideo()voidA small mirrored camera preview, bottom left. hideVideo() keeps the <video> in the document, invisible at 2×2 px, and takes it back from any plugin that borrowed it — Chrome delivers camera frames only to a rendered video, so hiding it with display: none would stop the tracker.
showPredictions() / hidePredictions()voidA dot at the current gaze estimate.
resetCalibration()voidDiscard all points and the fitted map.
calibratePoint(x, y, embeddings?, captureMs?, timeoutMs?)Promise<number>Add one point at a viewport pixel location, collecting embeddings for captureMs (default 500) when none are supplied. Rejects with no camera frames for <ms> ms if a single frame takes longer than timeoutMs (default 5000). Returns how many were recorded.
fitCalibration(lambda?){lambda, nPoints} | nullFit the ridge map. Required after manual calibratePoint calls.
getCalibrationPoints()CalPoint[]Targets in viewport fractions, with their embeddings.
getCurrentPrediction(){x, y, t} | nullThe latest estimate, in viewport pixels.
onGazeUpdate(cb)() => voidSubscribe to every estimate; returns an unsubscribe function.
getTimingOffset()number | nullThe offset currently subtracted from t.
setTimingOffset(ms | null)voidSet it manually. null turns the correction off.
getLastLoopback()LoopbackResult | nullThe full result of the last time-sync run.
onSetupProgress(cb)() => voidSubscribe to start()'s load progress — see SaccadeProgress. The most recent report is replayed on subscribe; the returned function unsubscribes. Nothing is reported for a tracker supplied through the tracker parameter.
getSetupProgress()SaccadeProgress | nullThe most recent progress report.
dispose()voidEnd the frame subscriptions, remove the camera preview and gaze dot, and dispose the tracker (releasing the camera) unless it was supplied through the tracker parameter. jsPsych has no extension teardown hook, so a page that runs more than one experiment has to call this between them.

Example

const jsPsych = initJsPsych({
extensions: [{ type: jsPsychExtensionSaccade, params: { tta: 1 } }],
});

timeline.push({
type: jsPsychHtmlKeyboardResponse,
stimulus: "<img id='stim' src='face.png'>",
extensions: [{ type: jsPsychExtensionSaccade, params: { targets: ["#stim"] } }],
});

Trial order

timeline.push({ type: jsPsychSaccadePreview });    // camera and model
timeline.push({ type: jsPsychSaccadeTimeSync }); // needs a camera
timeline.push({ type: jsPsychSaccadeCalibrate }); // needs a camera
timeline.push({ type: jsPsychSaccadeValidate }); // needs a fitted calibration