Where to click?

Hi everyone,

I’m new to Anki and I would like to create “find-the-spot” image cards (similar to “Where to click” questions).

For example, I would like to use a screenshot of a dashboard and ask a question like:

“Where do I need to click to access the advanced settings?”

The idea is that I can click on a specific area within the image that I previously defined as correct, and Anki would then tell me whether my click was correct or incorrect.

Is this possible in Anki? If so, how can I set this up?

Thank you in advance!

It certainly is possible.
Unfortunately, I’m not aware of any existing solutions, so making one from scratch seems to be the most straightforward option. It would probably be convenient to repurpose an Image Occlusion note for the task. You can add one by going to ToolsManage Note TypesAddAdd: Image Occlusion and giving it an appropriate name. Then in the template editor (opened by clicking Cards in the same menu), replace the code with the following pieces:

  1. Front Template
    <div hidden>{{cloze:Occlusion}}</div>
    <div id="image-occlusion-container" style="aspect-ratio: 1 / 1;">
        <div id="point" hidden></div>
        {{Image}}
        <canvas id="image-occlusion-canvas"></canvas>
    </div>
    
    <script>
    cont = document.getElementById("image-occlusion-container");
    pt = document.getElementById("point");
    
    localStorage.removeItem("click-coords");
    function saveClick(x, y) {
      localStorage.setItem("click-coords", JSON.stringify({ x, y }));
    }
    function displayClick(x, y) {
      pt.style.left = `${100 * x}%`;
      pt.style.top = `${100 * y}%`;
      pt.removeAttribute("hidden");
    }
    
    cont.addEventListener('mousedown', (e)=>{
      const rect = cont.getBoundingClientRect();
      const x = (e.clientX - rect.left) / rect.width;
      const y = (e.clientY - rect.top) / rect.height;
      saveClick(x, y);
      displayClick(x, y);
    });
    </script>
    
  2. Back Template
    <div hidden>{{cloze:Occlusion}}</div>
    <div id="image-occlusion-container" style="aspect-ratio: 1 / 1;">
        <div id="frame"></div>
        <div id="point" hidden></div>
        {{Image}}
        <canvas id="image-occlusion-canvas"></canvas>
    </div>
    <script>
    clozeDataL = document.querySelector(".cloze-highlight");
    ["top", "left", "width", "height"].forEach(dim => {
      document.getElementById("frame").style[dim] = `${100 * clozeDataL.getAttribute("data-"+dim)}%`;
    });
    </script>
    <script>
    pt = document.getElementById("point");
    function displayClick(x, y) {
      pt.style.left = `${100 * x}%`;
      pt.style.top = `${100 * y}%`;
      pt.removeAttribute("hidden");
    }
    coord = JSON.parse(localStorage.getItem("click-coords"));
    if (coord) {
      displayClick(coord.x, coord.y);
    }
    </script>
    
  3. Styling
    #frame {
        position: absolute;
        z-index: 1000;
        box-sizing: border-box;
        border: 3px solid purple;
    }
    
    #point {
        width: 10px;
        height: 10px;
        position: absolute;
        box-sizing: border-box;
        background: gold;
        border-radius: 100px;
        z-index: 2000;
        transform: translate(-50%, -50%);
    }
    

This will implement the most basic functionality of saving and displaying mouse click position on an image. The backside will load the click made on the front and display the frame around the correct region, based on which a card can be rated. The region itself can be defined during card creation with a rectangular mask thanks to the native Image Occlusion interface.

This, of course, can be build upon to support non-rectangular and rotated regions, make card autoflip to the back on click or being automatically rated based on whether the click is located inside the correct region or not, etc.

6 Likes

Thank you so, so much – this is exactly what I was looking for! :folded_hands:

All my previous attempts with AI completely failed. This really shows that humans can’t be replaced that easily :grinning_face_with_smiling_eyes:

There’s just one small thing I noticed:

I suspect the issue might be in the Front Template. When I insert a rectangular image, it gets stretched into a square during review. I assume this is caused by style=“aspect-ratio: 1 / 1;”.

Would it be possible to adjust this so the original aspect ratio of the image is preserved?

Unfortunately, I’m not sure how the code would need to be modified correctly to achieve that.

Thanks again for all the effort and your help – I really appreciate it!

Best regards

2 Likes

My bad. Here is the corrected version:

  1. Front
    <div hidden>{{cloze:Occlusion}}</div>
    <div id="image-occlusion-container">
        <div id="point" hidden></div>
        {{Image}}
    </div>
    
    <script>
    cont = document.getElementById("image-occlusion-container");
    pt = document.getElementById("point");
    
    localStorage.removeItem("click-coords");
    function saveClick(x, y) {
      localStorage.setItem("click-coords", JSON.stringify({ x, y }));
    }
    function displayClick(x, y) {
      pt.style.left = `${100 * x}%`;
      pt.style.top = `${100 * y}%`;
      pt.removeAttribute("hidden");
    }
    
    cont.addEventListener('mousedown', (e)=>{
      const rect = cont.getBoundingClientRect();
      const x = (e.clientX - rect.left) / rect.width;
      const y = (e.clientY - rect.top) / rect.height;
      saveClick(x, y);
      displayClick(x, y);
    });
    </script>
    
  2. Back
    <div hidden>{{cloze:Occlusion}}</div>
    <div id="image-occlusion-container">
        <div id="frame"></div>
        <div id="point" hidden></div>
        {{Image}}
    </div>
    
    <script>
    clozeDataL = document.querySelector(".cloze-highlight");
    ["top", "left", "width", "height"].forEach(dim => {
      document.getElementById("frame").style[dim] = `${100 * clozeDataL.getAttribute("data-"+dim)}%`;
    });
    </script>
    <script>
    pt = document.getElementById("point");
    function displayClick(x, y) {
      pt.style.left = `${100 * x}%`;
      pt.style.top = `${100 * y}%`;
      pt.removeAttribute("hidden");
    }
    coord = JSON.parse(localStorage.getItem("click-coords"));
    if (coord) {
      displayClick(coord.x, coord.y);
    }
    </script>
    
  3. Styling
    #image-occlusion-container {
        position: relative;
        max-height: unset;
        width: max-content;
        margin: 0 auto;
    }
    
    #image-occlusion-container img {
        position: static;
        max-width: 80vw;
        max-height: calc(95vh - 40px);
    }
    
    #frame {
        position: absolute;
        z-index: 1000;
        box-sizing: border-box;
        border: 3px solid purple;
    }
    
    #point {
        width: 10px;
        height: 10px;
        position: absolute;
        box-sizing: border-box;
        background: gold;
        border-radius: 100px;
        z-index: 2000;
        transform: translate(-50%, -50%);
    }
    

Feel free to point out anything you find wrong in this iteration.

1 Like