Hello everyone
Today I purchased ankimobile, the reason is that I’m taking a one-week holiday and I have a lot of reviews to do, which would be easier to be done on an Ipad screen than on a little phone screen.
I found out that anki image occlusion feature has (in my humble opinion) a major flaw: it’s not possible to zoom in images. With things like mind maps, it makes it impossible to make cards out of them.
I gave Claude the original code, which fixed the problem both on PC and Android. It’s not perfect, and has some flaws, but overall works.
The major issue is that it does not work at all on ankimobile. It does not even show the card. If anyone has any idea, or would like to review the code and help with it, I’d appreciate it. I hope that in the future the ‘zoom in’ function will be implemented natively
Here’s the code, both for front and back of the card:
{{#Header}}<div>{{Header}}</div>{{/Header}}
<div style="display: none">{{cloze:Occlusion}}</div>
<div id="err"></div>
<div id="image-occlusion-container">
{{Image}}
<canvas id="image-occlusion-canvas"></canvas>
</div>
<script>
function initializeImageOcclusion() {
try {
anki.imageOcclusion.setup();
const container = document.getElementById('image-occlusion-container');
const canvas = document.getElementById('image-occlusion-canvas');
let img = null;
let scale = 1;
let originX = 0;
let originY = 0;
let isDragging = false;
let startX, startY;
let masksVisible = true;
let lastPinchDistance = 0;
let lastTouchX, lastTouchY;
const MIN_SCALE = 0.1;
const MAX_SCALE = 10;
function findImage() {
return container.querySelector('img') || document.querySelector('#image-occlusion-container img');
}
function waitForImage(callback, maxAttempts = 10, interval = 100) {
let attempts = 0;
const checkImage = () => {
img = findImage();
if (img) {
callback();
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(checkImage, interval);
} else {
throw new Error("Image not found after maximum attempts");
}
};
checkImage();
}
function saveZoomState() {
const state = { scale, originX, originY };
localStorage.setItem('zoomState', JSON.stringify(state));
}
function loadZoomState() {
const savedState = localStorage.getItem('zoomState');
if (savedState) {
const state = JSON.parse(savedState);
scale = state.scale;
originX = state.originX;
originY = state.originY;
setTransform(0);
}
}
function setTransform(duration = 0) {
if (!img) return;
const transform = `translate(${originX}px, ${originY}px) scale(${scale})`;
[img, canvas].forEach(el => {
el.style.transform = transform;
el.style.transition = `transform ${duration}ms ease-out`;
});
saveZoomState();
}
function limitZoom(value) {
return Math.min(Math.max(value, MIN_SCALE), MAX_SCALE);
}
function handleZoom(delta, centerX, centerY) {
const newScale = limitZoom(scale + delta);
const rect = container.getBoundingClientRect();
const mouseX = centerX - rect.left;
const mouseY = centerY - rect.top;
originX = originX - (mouseX / scale - mouseX / newScale);
originY = originY - (mouseY / scale - mouseY / newScale);
scale = newScale;
setTransform(100);
}
function handleWheel(event) {
if (event.shiftKey) {
event.preventDefault();
const delta = event.deltaY > 0 ? -0.1 : 0.1;
handleZoom(delta, event.clientX, event.clientY);
}
}
function handleMouseDown(event) {
isDragging = true;
startX = event.clientX - originX;
startY = event.clientY - originY;
container.style.cursor = 'grabbing';
}
function handleMouseMove(event) {
if (isDragging) {
originX = event.clientX - startX;
originY = event.clientY - startY;
setTransform();
}
}
function handleMouseUp() {
isDragging = false;
container.style.cursor = 'grab';
}
function handleTouchStart(event) {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
lastPinchDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY);
} else if (event.touches.length === 1) {
isDragging = true;
const touch = event.touches[0];
startX = touch.clientX - originX;
startY = touch.clientY - originY;
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
}
}
function handleTouchMove(event) {
event.preventDefault();
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const pinchDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY);
const delta = (pinchDistance - lastPinchDistance) * 0.01;
lastPinchDistance = pinchDistance;
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
handleZoom(delta, centerX, centerY);
} else if (event.touches.length === 1 && isDragging) {
const touch = event.touches[0];
const deltaX = touch.clientX - lastTouchX;
const deltaY = touch.clientY - lastTouchY;
originX += deltaX;
originY += deltaY;
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
setTransform();
}
}
function handleTouchEnd(event) {
if (event.touches.length < 2) {
lastPinchDistance = 0;
}
if (event.touches.length === 0) {
isDragging = false;
}
}
function handleKeyDown(event) {
if (event.key === 'Escape') {
scale = 1;
originX = 0;
originY = 0;
setTransform(300);
}
}
let rafId = null;
function optimizedHandleMouseMove(event) {
if (isDragging) {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => handleMouseMove(event));
}
}
function toggleMasks() {
masksVisible = !masksVisible;
canvas.style.display = masksVisible ? 'block' : 'none';
}
function setupEventListeners() {
container.addEventListener('wheel', handleWheel, { passive: false });
container.addEventListener('mousedown', handleMouseDown);
container.addEventListener('mousemove', optimizedHandleMouseMove);
container.addEventListener('mouseup', handleMouseUp);
container.addEventListener('mouseleave', handleMouseUp);
container.addEventListener('touchstart', handleTouchStart);
container.addEventListener('touchmove', handleTouchMove, { passive: false });
container.addEventListener('touchend', handleTouchEnd);
document.addEventListener('keydown', handleKeyDown);
container.setAttribute('tabindex', '0');
container.setAttribute('aria-label', 'Immagine zoomabile e spostabile');
container.style.cursor = 'grab';
const toggleButton = document.getElementById('toggle');
if (toggleButton) {
toggleButton.addEventListener('click', toggleMasks);
}
}
function initialize() {
loadZoomState();
setupEventListeners();
}
waitForImage(initialize);
} catch (exc) {
document.getElementById("err").innerHTML = `Error loading image occlusion. Is your Anki version up to date?<br><br>${exc}`;
console.error("Image Occlusion Error:", exc);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeImageOcclusion);
} else {
initializeImageOcclusion();
}
</script>
<div><button id="toggle">Toggle Masks</button></div>
{{#Back Extra}}<div>{{Back Extra}}</div>{{/Back Extra}}{{#Header}}<div>{{Header}}</div>{{/Header}}
<div style="display: none">{{cloze:Occlusion}}</div>
<div id="err"></div>
<div id="image-occlusion-container">
{{Image}}
<canvas id="image-occlusion-canvas"></canvas>
</div>
<script>
function initializeImageOcclusion() {
try {
anki.imageOcclusion.setup();
const container = document.getElementById('image-occlusion-container');
const canvas = document.getElementById('image-occlusion-canvas');
let img = null;
let scale = 1;
let originX = 0;
let originY = 0;
let isDragging = false;
let startX, startY;
let masksVisible = true;
let lastPinchDistance = 0;
let lastTouchX, lastTouchY;
const MIN_SCALE = 0.1;
const MAX_SCALE = 10;
function findImage() {
return container.querySelector('img') || document.querySelector('#image-occlusion-container img');
}
function waitForImage(callback, maxAttempts = 10, interval = 100) {
let attempts = 0;
const checkImage = () => {
img = findImage();
if (img) {
callback();
} else if (attempts < maxAttempts) {
attempts++;
setTimeout(checkImage, interval);
} else {
throw new Error("Image not found after maximum attempts");
}
};
checkImage();
}
function saveZoomState() {
const state = { scale, originX, originY };
localStorage.setItem('zoomState', JSON.stringify(state));
}
function loadZoomState() {
const savedState = localStorage.getItem('zoomState');
if (savedState) {
const state = JSON.parse(savedState);
scale = state.scale;
originX = state.originX;
originY = state.originY;
setTransform(0);
}
}
function setTransform(duration = 0) {
if (!img) return;
const transform = `translate(${originX}px, ${originY}px) scale(${scale})`;
[img, canvas].forEach(el => {
el.style.transform = transform;
el.style.transition = `transform ${duration}ms ease-out`;
});
saveZoomState();
}
function limitZoom(value) {
return Math.min(Math.max(value, MIN_SCALE), MAX_SCALE);
}
function handleZoom(delta, centerX, centerY) {
const newScale = limitZoom(scale + delta);
const rect = container.getBoundingClientRect();
const mouseX = centerX - rect.left;
const mouseY = centerY - rect.top;
originX = originX - (mouseX / scale - mouseX / newScale);
originY = originY - (mouseY / scale - mouseY / newScale);
scale = newScale;
setTransform(100);
}
function handleWheel(event) {
if (event.shiftKey) {
event.preventDefault();
const delta = event.deltaY > 0 ? -0.1 : 0.1;
handleZoom(delta, event.clientX, event.clientY);
}
}
function handleMouseDown(event) {
isDragging = true;
startX = event.clientX - originX;
startY = event.clientY - originY;
container.style.cursor = 'grabbing';
}
function handleMouseMove(event) {
if (isDragging) {
originX = event.clientX - startX;
originY = event.clientY - startY;
setTransform();
}
}
function handleMouseUp() {
isDragging = false;
container.style.cursor = 'grab';
}
function handleTouchStart(event) {
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
lastPinchDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY);
} else if (event.touches.length === 1) {
isDragging = true;
const touch = event.touches[0];
startX = touch.clientX - originX;
startY = touch.clientY - originY;
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
}
}
function handleTouchMove(event) {
event.preventDefault();
if (event.touches.length === 2) {
const touch1 = event.touches[0];
const touch2 = event.touches[1];
const pinchDistance = Math.hypot(touch1.clientX - touch2.clientX, touch1.clientY - touch2.clientY);
const delta = (pinchDistance - lastPinchDistance) * 0.01;
lastPinchDistance = pinchDistance;
const centerX = (touch1.clientX + touch2.clientX) / 2;
const centerY = (touch1.clientY + touch2.clientY) / 2;
handleZoom(delta, centerX, centerY);
} else if (event.touches.length === 1 && isDragging) {
const touch = event.touches[0];
const deltaX = touch.clientX - lastTouchX;
const deltaY = touch.clientY - lastTouchY;
originX += deltaX;
originY += deltaY;
lastTouchX = touch.clientX;
lastTouchY = touch.clientY;
setTransform();
}
}
function handleTouchEnd(event) {
if (event.touches.length < 2) {
lastPinchDistance = 0;
}
if (event.touches.length === 0) {
isDragging = false;
}
}
function handleKeyDown(event) {
if (event.key === 'Escape') {
scale = 1;
originX = 0;
originY = 0;
setTransform(300);
}
}
let rafId = null;
function optimizedHandleMouseMove(event) {
if (isDragging) {
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => handleMouseMove(event));
}
}
function toggleMasks() {
masksVisible = !masksVisible;
canvas.style.display = masksVisible ? 'block' : 'none';
}
function setupEventListeners() {
container.addEventListener('wheel', handleWheel, { passive: false });
container.addEventListener('mousedown', handleMouseDown);
container.addEventListener('mousemove', optimizedHandleMouseMove);
container.addEventListener('mouseup', handleMouseUp);
container.addEventListener('mouseleave', handleMouseUp);
container.addEventListener('touchstart', handleTouchStart);
container.addEventListener('touchmove', handleTouchMove, { passive: false });
container.addEventListener('touchend', handleTouchEnd);
document.addEventListener('keydown', handleKeyDown);
container.setAttribute('tabindex', '0');
container.setAttribute('aria-label', 'Immagine zoomabile e spostabile');
container.style.cursor = 'grab';
const toggleButton = document.getElementById('toggle');
if (toggleButton) {
toggleButton.addEventListener('click', toggleMasks);
}
}
function initialize() {
loadZoomState();
setupEventListeners();
}
waitForImage(initialize);
} catch (exc) {
document.getElementById("err").innerHTML = `Error loading image occlusion. Is your Anki version up to date?<br><br>${exc}`;
console.error("Image Occlusion Error:", exc);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeImageOcclusion);
} else {
initializeImageOcclusion();
}
</script>
<div><button id="toggle">Toggle Masks</button></div>
{{#Back Extra}}<div>{{Back Extra}}</div>{{/Back Extra}}