Skip to content

Commit

Permalink
Merge pull request #47 from qmd-lab/feature-image-zoom
Browse files Browse the repository at this point in the history
Add image zooming feature
  • Loading branch information
andrewpbray authored Jul 27, 2024
2 parents c7090cb + 2b1590f commit a39411b
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 102 deletions.
8 changes: 4 additions & 4 deletions _extensions/closeread/closeread.lua
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@

quarto.log.output("===== Closeread Log =====")

-- set defaults
local debug_mode = false
local step_selectors = {["focus-on"] = true}
local cr_attributes = {["pan-to"] = true, ["scale-by"] = true}

-- Append attributes to any cr line blocks
function add_attributes(lineblock)
Expand Down Expand Up @@ -107,6 +106,7 @@ function make_sidebar_layout(div)
Block = function(block)
if is_sticky(block) then
block = shift_id_to_block(block)
block.classes:insert("sticky")
return block, false -- if a sticky element is found, don't process child blocks
else
return {}
Expand Down Expand Up @@ -174,14 +174,14 @@ function wrap_block(block)
-- extract attributes
local attributesToMove = {}
for attr, value in pairs(block.attributes) do
if step_selectors[attr] then
if step_selectors[attr] or cr_attributes[attr] then
attributesToMove[attr] = value
block.attributes[attr] = nil
end
end

-- finally construct a pandoc.div with the new details and content to return
return pandoc.Div(block, pandoc.Attr("", "", attributesToMove))
return pandoc.Div(block, pandoc.Attr("", {"step"}, attributesToMove))
end


Expand Down
14 changes: 6 additions & 8 deletions _extensions/closeread/grid.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@

/* extended wrapper around content to allow early scroll detection */
> * {
padding-block-start: 20svh;
padding-block-end: 20svh;
padding-block-start: 45svh;
padding-block-end: 45svh;
vertical-align: middle;

/* styling for actual content (*/
> * {
padding: 15px;
}
padding-left: 30px;
padding-right: 30px;

p {
margin-bottom: 0;
Expand All @@ -40,9 +37,10 @@
display: grid;
height: 100svh;
position: sticky;
overflow: hidden;
top: 0;

[id^="cr-"] {
.sticky {
grid-area: 1 / 1;
margin: auto;

Expand Down
148 changes: 58 additions & 90 deletions _extensions/closeread/scroller-init.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ document.addEventListener("DOMContentLoaded", () => {
document.body.classList.add("cr-debug")
}

/*
// define an ojs variable if the connector module is available
let focusedSticky = "none";
let focusedStickyName = "none";
const ojsModule = window._ojs?.ojsConnector?.mainModule
const ojsScrollerName = ojsModule?.variable();
const ojsScrollerProgress = ojsModule?.variable();
const ojsScrollerDirection = ojsModule?.variable();
ojsScrollerName?.define("crScrollerName", focusedSticky);
ojsScrollerName?.define("crScrollerName", focusedStickyName);
ojsScrollerProgress?.define("crScrollerProgress", 0);
ojsScrollerDirection?.define("crScrollerDirection", null);
if (ojsModule === undefined) {
console.error("Warning: Quarto OJS module not found")
}
*/

const allStickies = Array.from(document.querySelectorAll("[id^='cr-']"));
const scroller = scrollama();
Expand All @@ -42,48 +44,37 @@ document.addEventListener("DOMContentLoaded", () => {
debug: debugMode
})
.onStepEnter((response) => {

if (response.direction == "down") {
focusedStickyName = "cr-" + response.element.getAttribute("data-focus-on");
ojsScrollerName?.define("crScrollerName", focusedStickyName);

// applyFocusOn
allStickies.forEach(node => {node.classList.remove("cr-active")});
const focusedSticky = document.querySelectorAll("[id=" + focusedStickyName)[0]
focusedSticky.classList.add("cr-active");

// applyHighlightSpans
highlightSpans(focusedSticky, response.element);
}
})
.onStepExit((response) => {

if (response.direction == "up") {
focusedStickyName = "cr-" + response.element.getAttribute("data-focus-on");
ojsScrollerName?.define("crScrollerName", focusedStickyName);

// applyFocusOn
allStickies.forEach(node => {node.classList.remove("cr-active")});
const focusedSticky = document.querySelectorAll("[id=" + focusedStickyName)[0]
focusedSticky.classList.add("cr-active");

// applyHighlightSpans
highlightSpans(focusedSticky, response.element);
}

updateStickies(allStickies, response);
})
.onStepProgress((response) => {
// { element, index, progress }
/*
ojsScrollerProgress?.define("crScrollerProgress",
response.progress);
ojsScrollerDirection?.define("crScrollerDirection",
response.direction);
*/
});

// also recalc transitions and highlights on window resize
//window.addEventListener("resize", d => updateStickies(allStickies, allSteps));

});

/* updateStickies: fill in with description */
function updateStickies(allStickies, response) {
focusedStickyName = "cr-" + response.element.getAttribute("data-focus-on");
//ojsScrollerName?.define("crScrollerName", focusedStickyName);
const focusedSticky = document.querySelectorAll("[id=" + focusedStickyName)[0];

// update which sticky is active
allStickies.forEach(node => {node.classList.remove("cr-active")});
focusedSticky.classList.add("cr-active");

// apply additional effects
highlightSpans(focusedSticky, response.element);
transformSticky(focusedSticky, response.element);
}


function highlightSpans(stickyEl, stepEl) {
Expand Down Expand Up @@ -116,64 +107,6 @@ function highlightSpans(stickyEl, stepEl) {
}



/* updateStickies: recalculates which sticky elements (between the first
and the one before `indexTo`) need to be displayed, and gives them the class
`.cr-active`. All elements first have that class removed regardless of
position.
(note that sticky elements use the `cr-id` attribute on the user side, but it
appears in the rendered html as `data-cr-id`.) */
function updateStickies(allStickies, activeSticky) {

// applyFocusOn(allStickies, activeSticky);
// applyHighlight(allStickies, activeSticky);
// applyZoom(allStickies, activeSticky);


// reset all elements
allStickies.forEach(node => node.classList.remove("cr-active"));

// replay the vertical transition progress: remove sticky targets if they've
// been transitioned `from` and add them back if they're transitioned `to`
// NOTE - why is this only triggering once when
/// priorSteps.length > 1?
stickiesToEnable = new Set();
priorSteps.forEach(node => {

// from/to comma-sep strings -> arrays -> sets -> remove/add stickies
const nodeFromIDs = node.getAttribute("data-cr-from");
const nodeToIDs = node.getAttribute("data-cr-to");
(new Set(nodeFromIDs?.split(/,\s*/)))
.forEach(id => stickiesToEnable.delete(id));
(new Set(nodeToIDs?.split(/,\s*/)))
.forEach(id => stickiesToEnable.add(id));

});

// each sticky left post-replay needs to be enabled, if it is valid, and then
// focus effects need to be applied
stickiesToEnable.forEach(stickyId => {
const targets = document.querySelectorAll("[data-cr-id=" + stickyId + "]")

if (targets.length == 0) {
throw Error("Can't find cr-id=" + stickyId +
". Please ensure the element you're transitioning to exists.")
}
if (targets.length > 1) {
throw Error("Multiple elements with cr-id=" + stickyId +
". Please ensure cr-id attributes are unique.")
}

// do the visibility update
targets[0].classList.add("cr-active")
if (targets[0].classList.contains("cr-poem")) {
updateActivePoem(targets[0], priorSteps)
}

})

}

// make the given element active. if it's a poem, rescale it
function updateActivePoem(el, priorSteps) {

Expand Down Expand Up @@ -292,5 +225,40 @@ function scalePoemToSpan(el, highlightIds, paddingX = 75, paddingY = 50) {
// apply styles
el.style.setProperty("transform",
`matrix(${scale}, 0, 0, ${scale}, 0, ${centerDeltaY})`)
}


//=================//
// Transform Sticky //
//=================//

}
function transformSticky(sticky, step) {

// initialize empty strings
let translateStr = "";
let scaleStr = "";
let transformStr = "";

if (step.hasAttribute("data-pan-to")) {
// get translate attributes from step
translateStr = "translate(" + step.getAttribute("data-pan-to") + ")";
}

if (step.hasAttribute("data-scale-by")) {
// get scale attributes from step
scaleStr = "scale(" + step.getAttribute("data-scale-by") + ")";
}

// form transform string
if (translateStr && scaleStr) {
transformStr = translateStr + " " + scaleStr;
} else if (translateStr) {
transformStr = translateStr;
} else if (scaleStr) {
transformStr = scaleStr;
}

// and use it to scale the sticky
sticky.style.transform = transformStr;

}
Binary file added docs/gallery/demos/minard-zoom/minard-map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions docs/gallery/demos/minard-zoom/minard.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
title: "Minard's Map"
format:
closeread-html
---


:::{.cr-layout}

There once was a French engineer names Charles Minard who put his mind to creating a map that illustrated Napoleon's retreat from Moscow.

:::{focus-on="map"}
Here is Minard's Map.
:::

:::{#cr-map}
![](minard-map.png)
:::

:::{focus-on="map" pan-to="-30%,0%" scale-by="2.6"}
Here is the city of Smolensk.
:::

:::{focus-on="map" pan-to="40%,110%" scale-by="2.6"}
Here is the signature of the creator: Monsieur Minard.
:::

:::{focus-on="map" style="padding-block-end: 85svh"}
It is a masterpiece of data visualization.
:::

:::

## Le Fin

0 comments on commit a39411b

Please sign in to comment.