I’ve written this from seeing the common trouble of seeing big images such as slideshows as seen from this thread. For the time being, I’m satisfied with being able to click/tap and pan around. I won’t be adding further functionality like panning by dragging the mouse. I’m licensing this as 0BSD so feel free to adopt this into your cool card template or addon, i don’t need attribution.
Changelog
- 2025-02-14 #1: Fixed non-centered magnified image. whoops.
Demonstration: Basic front side card template
<script>
'use strict';
var isAnkiweb;
var qaElm;
var cssEval;
var magnifyElm;
function getCSSProperty(elm, prop, default_ = undefined) {
return getComputedStyle(elm).getPropertyValue(prop) || default_;
}
function computeCSSLength (v) {
// See also: https://drafts.csswg.org/cssom/#resolved-values
cssEval.style.height = v;
return getComputedStyle(cssEval).height;
}
function isSVG(img) {
const src = img.src.toLowerCase()
return src.endsWith('.svg') || src.startsWith('data:image/svg+xml') || src.match(/\.svg[?#]/);
}
function isMagnifyAble(img, heightThreshold, widthThreshold) {
return img.hasAttribute('data-magnify') || img.naturalHeight >= heightThreshold || img.naturalWidth >= widthThreshold;
}
var styleImgSize = (width, height) =>
`width: calc(${width}px * var(--magnify-zoom)); height: calc(${height}px * var(--magnify-zoom));`;
function magnifyImg(ev) {
ev.stopPropagation();
const magnifyImg = magnifyElm.querySelector('img');
const originImg = ev.currentTarget;
let styleAttr;
if (isSVG(originImg)) {
styleAttr = `${styleAttr} ${styleImgSize(originImg.width, originImg.height)}`
} else {
styleAttr = `${styleAttr} ${styleImgSize(originImg.naturalWidth, originImg.naturalHeight)}`;
}
const zoomAttr = originImg.getAttribute('data-magnify-zoom');
if (zoomAttr !== null) {
styleAttr = `${styleAttr} --magnify-zoom: ${zoomAttr};`;
}
magnifyImg.setAttribute('style', styleAttr);
magnifyImg.src = originImg.src;
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
if (scrollbarWidth > 0) {
magnifyElm.setAttribute('style', `--scrollbar-width: ${scrollbarWidth}px;`);
}
magnifyElm.hidden = false;
}
function magnifyClose(ev) {
magnifyElm.hidden = true;
}
function setup() {
const magnifyElm_ = document.querySelector('#magnify:not(template *)');
if (magnifyElm_ !== null) {
magnifyElm_.hidden = true;
return;
}
isAnkiweb = location.hostname.includes('ankiuser.net') || location.hostname.includes('ankiweb.net');
qaElm = document.querySelector('#qa');
if (isAnkiweb) {
document.documentElement.classList.add('ankiweb');
}
qaElm.querySelectorAll('.to-body-end')
.forEach((tp) => qaElm.appendChild(tp.content.cloneNode(true)));
cssEval = qaElm.querySelector('#css-eval:not(template *)');
magnifyElm = qaElm.querySelector('#magnify:not(template *)');
magnifyElm.querySelector('button').addEventListener('click', magnifyClose);
}
function main() {
setup();
qaElm.querySelectorAll('img:not(template *, #magnify *)').forEach((img) => {
const heightThreshold = parseInt(computeCSSLength(
getCSSProperty(img, '--magnify-height-threshold', '100vh')));
const widthThreshold = parseInt(computeCSSLength(
getCSSProperty(img, '--magnify-width-threshold', '100vw')));
if (img.complete) {
if (isSVG(img) || isMagnifyAble(img, heightThreshold, widthThreshold)) {
img.classList.add('can-magnify');
img.addEventListener('click', magnifyImg);
}
} else {
img.addEventListener('load', () => {
if (isSVG(img) || isMagnifyAble(img, heightThreshold, widthThreshold)) {
img.classList.add('can-magnify');
img.addEventListener('click', magnifyImg);
}
});
}
});
}
if (document.querySelector('.card') !== null) {
console.log('[Init] .card is ready when loading <script>');
if (document.readyState === 'loading') {
console.log('[Init] document is still loading, deferring to "DOMContentLoaded" event');
document.addEventListener('DOMContentLoaded', main);
} else {
console.log('[Init] document is ready')
main();
}
} else {
console.log('[Init] deferring to next macrotask');
setTimeout(main);
}
</script>
<style>
.card {
--border-radius-large: 0.7em;
}
.can-magnify:hover {
cursor: zoom-in;
box-shadow: 0 0 1em var(--shadow);
transition: box-shadow 0.25s ease-out;
}
@media (max-width: 768px) {
.can-magnify {
box-shadow: 0 0 0.5em var(--shadow);
}
}
#magnify[hidden] {
display: none !important;
}
#magnify {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
max-width: none;
max-height: none;
background-color: hsla(0, 0%, 0%, 0.5);
background-clip: padding-box;
overflow: auto;
z-index: 10;
}
/* AnkiWeb hacks */
.ankiweb #magnify {
z-index: 2010;
}
.ankiweb #ansarea {
z-index: 2000;
}
#magnify > img {
display: block;
max-width: none;
max-height: none;
margin: auto;
}
#magnify > button {
--border-strong: hsl(0, 90%, 75%);
--border-subtle: hsl(0, 90%, 80%);
--button-bg: hsl(0, 90%, 95%);
--button-gradient-start: hsl(0, 100%, 97%);
--button-gradient-end: hsl(0, 90%, 95%);
position: fixed;
top: 0;
right: 0;
margin: 0;
margin-top: 2em;
margin-right: calc(2em + var(--scrollbar-width, 0px));
padding: 1em;
background: var(--button-bg);
border-radius: var(--border-radius-large);
border: 1px solid var(--border-subtle);
box-shadow: 0px 2px 1px -1px rgba(20,20,20,.12),0px 1px 1px 0px rgba(20,20,20,.06),0px 1px 3px 0px rgba(20,20,20,.04);
opacity: 0.2;
transition: opacity 0.15s ease-out;
font-size: 0.8em;
}
#magnify > button:hover {
cursor: pointer;
opacity: 1;
background: linear-gradient(180deg, var(--button-gradient-start) 0%, var(--button-gradient-end) 100%);
border: 1px solid var(--border-strong);
box-shadow: 0px 3px 1px -2px rgba(20,20,20,.2),0px 2px 2px 0px rgba(20,20,20,.14),0px 1px 5px 0px rgba(20,20,20,.12);
}
.nightMode #magnify > button {
--border-strong: hsl(0, 70%, 45%);
--border-subtle: hsl(0, 70%, 40%);
--button-bg: hsl(0, 75%, 22%);
--button-gradient-start: hsl(0, 70%, 27%);
--button-gradient-end: hsl(0, 75%, 22%);
}
@media (max-width: 768px) {
#magnify > button {
font-size: 1em;
}
}
</style>
<template class="to-body-end">
<div id="magnify" hidden>
<button>Exit<br>Zoom</button>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw== ">
</div>
<div id="css-eval" hidden></div>
</template>
{{Front}}
Demonstration: Basic back side card template
{{FrontSide}}
<hr id=answer>
{{Back}}
Demonstration: CSS for card template
.card {
font-family: arial;
font-size: 20px;
text-align: center;
color: black;
background-color: white;
/* Images larger than 50% of your screen height can be magnified */
--magnify-height-threshold: 50vh;
/* Images larger than 50% of your screen width can be magnified */
--magnify-width-threshold: 50vw;
/* Default zoom factor, must be in floating point.
* You can set manually for each image, e.g. <img data-magnify-zoom="2.0" src=...>
* to zoom it to 2x the size.
*/
--magnify-zoom: 1.5;
}
Demonstration: Sample Images
These looks pretty much the same even in AnkiWeb and AnkiDroid. AnkiWeb is a bit hacky though.