visualizing-human-and-neura.../src/Visualization.svelte

203 lines
5.1 KiB
Svelte

<script>
export let isAnimated = false;
import FilePicker from './FilePicker.svelte';
import { onMount, onDestroy, tick as nextTick } from 'svelte';
import { config } from './stores.js';
import { aggregateWordFixations } from './helpers';
let observer, canvas, progress, ctx, show_sequential, fixation_data, sequential_fixation_data, fixation_backdrop, data, container;
$: {
data = (show_sequential) ? sequential_fixation_data : fixation_data;
reset();
}
let i = 0, t = null, isInitialized = false;
onMount(() => {
ctx = canvas.getContext('2d');
observer = new ResizeObserver( entries => {
canvas.height = entries[0].contentRect.height;
canvas.width = entries[0].contentRect.width;
});
observer.observe(container);
});
onDestroy(() => {
observer.disconnect();
});
async function onLoad(event) {
fixation_data = event.detail.fixation_data;
sequential_fixation_data = aggregateWordFixations(event.detail.fixation_data);
fixation_backdrop = event.detail.image;
isInitialized = true;
await nextTick();
//config.update(c => {c.$config.zoom = canvas.width/fixation_backdrop.width; return c;});
requestAnimationFrame(tick);
}
function tick(time) {
if (!data || !fixation_backdrop) {
requestAnimationFrame(tick);
return;
}
let dt;
if (isAnimated){
if (t == null) t = time;
dt = (time - t)*$config.speed/100
if (dt > data[i].dur) {
i = (i<data.length-1)?i+1:0;
t = time;
progress.style.width = (100/data.length*i)+'%';
}
} else {
dt = data[i].dur;
progress.style.width = (100/data.length*i)+'%';
}
ctx.clearRect(0, 0, 5000, 5000);
ctx.save();
drawBackdrop();
drawFixation(data[i], dt);
drawTrace(data.slice((i-$config.trace_length>0)?i-$config.trace_length:0,i+1))
ctx.restore();
requestAnimationFrame(tick);
}
export function reset() {
i = 0;
t = null;
}
function transform({ x, y }) {
// transforms coordinates from image space into drawing space
return [$config.zoom/100 * x + $config.translatex, $config.zoom/100 * y + $config.translatey]
}
function drawBackdrop() {
ctx.drawImage(fixation_backdrop, $config.translatex, $config.translatey, $config.zoom/100*fixation_backdrop.width, $config.zoom/100*fixation_backdrop.height);
}
function drawFixation({ x, y, dur }, dt) {
ctx.save();
ctx.beginPath();
let radius = Math.pow(dt/dur, $config.duration_exponent)*dur*$config.duration_multiplier;
let [xt, yt] = transform({x, y});
let gradient;
try {
gradient = ctx.createRadialGradient(xt,yt,0, xt,yt,radius);
} catch(e) {
console.error(e);
return;
}
gradient.addColorStop(0, 'rgb(200, 0, 0, 0.5)');
gradient.addColorStop(1, 'rgb(200, 0, 0, 0)');
ctx.fillStyle = gradient;
ctx.arc(xt, yt, radius, 0, Math.PI*2, true);
ctx.fill();
ctx.restore();
}
function drawTrace(trace) {
ctx.save()
ctx.lineWidth = $config.trace_width;
let alpha = 0.2 + 0.8/$config.trace_length;
let i=0, xt, yt, xtn, ytn;
for (i; i<trace.length-1; i++) {
let { x, y } = trace[i];
let { x:xn, y:yn } = trace[i+1];
[xt, yt] = transform({x, y});
[xtn, ytn] = transform({x:xn, y:yn});
ctx.strokeStyle = `rgba(0,0,0,${alpha}`;
ctx.beginPath();
ctx.moveTo(xt, yt);
ctx.arc(xt, yt, $config.trace_radius, 0, Math.PI*2, true);
ctx.lineTo(xtn, ytn);
ctx.stroke();
alpha += 0.8/$config.trace_length;
}
ctx.beginPath();
ctx.arc(xt, yt, $config.trace_radius, 0, Math.PI*2, true);
ctx.stroke();
ctx.restore();
}
function translate(e) {
if (e.buttons == 1) {
$config.translatex += e.movementX;
$config.translatey += e.movementY;
}
}
function setZoom(e) {
$config.zoom += (e.deltaY > 0) ? -1 : 1;
}
</script>
<div bind:this={container} id="container">
{#if !isInitialized}
<FilePicker on:load={onLoad} />
{/if}
<span id="loadFile" class:hide={!isInitialized} title="Close and load other files" on:click={() => isInitialized = false}>🞩</span>
<div id="controls" class:hide={!isInitialized}>
<label title="Aggregates multiple (re-)visits into one fixation per word. Averages the fixation position and plays back in word order. Use to compare to fixation prediction models."><input type="checkbox" bind:checked={show_sequential}> As sequential word fixations</label>
</div>
<canvas class:hide={!isInitialized} bind:this={canvas} on:mousemove={translate} on:wheel|preventDefault={setZoom} height="1080" width="1920">This browser does not support canvas animations, which are required to render the visualization.</canvas>
<div id="progress" bind:this={progress}></div>
</div>
<style>
canvas {
margin: auto;
}
label {
font-weight: normal;
}
#loadFile {
position: absolute;
top: 0;
right: 0;
cursor: pointer;
margin: 10px;
}
#container {
position: relative;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
#progress {
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 20px;
background: #9b4dca;
}
#controls {
position: absolute;
right: 0;
bottom: 0;
text-align: right;
background: rgba(255,255,255,0.5);
padding: 10px;
}
.hide {
display: none;
}
</style>