diff --git a/_extensions/closeread/closeread.lua b/_extensions/closeread/closeread.lua index b18b3d6..0ac0b85 100644 --- a/_extensions/closeread/closeread.lua +++ b/_extensions/closeread/closeread.lua @@ -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) @@ -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 {} @@ -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 diff --git a/_extensions/closeread/grid.scss b/_extensions/closeread/grid.scss index 3124bd8..eb78918 100644 --- a/_extensions/closeread/grid.scss +++ b/_extensions/closeread/grid.scss @@ -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; @@ -40,9 +37,10 @@ display: grid; height: 100svh; position: sticky; + overflow: hidden; top: 0; - [id^="cr-"] { + .sticky { grid-area: 1 / 1; margin: auto; diff --git a/_extensions/closeread/scroller-init.js b/_extensions/closeread/scroller-init.js index 79fce20..74fe121 100644 --- a/_extensions/closeread/scroller-init.js +++ b/_extensions/closeread/scroller-init.js @@ -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(); @@ -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) { @@ -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) { @@ -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 // +//=================// -} \ No newline at end of file +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; + +} diff --git a/docs/gallery/demos/minard-zoom/minard-map.png b/docs/gallery/demos/minard-zoom/minard-map.png new file mode 100644 index 0000000..69c3288 Binary files /dev/null and b/docs/gallery/demos/minard-zoom/minard-map.png differ diff --git a/docs/gallery/demos/minard-zoom/minard.qmd b/docs/gallery/demos/minard-zoom/minard.qmd new file mode 100644 index 0000000..355f277 --- /dev/null +++ b/docs/gallery/demos/minard-zoom/minard.qmd @@ -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 \ No newline at end of file