UNPKG

slipshow

Version:

This is not another slide engine, but a slip engine.

1,383 lines (1,332 loc) 90.5 kB
var Slipshow = (function (exports) { 'use strict'; class SlipFigure extends HTMLImageElement { // Pour spécifier les attributs qui, changés, appellent "attributeChangedCallback" static get observedAttributes() { return ['figure-name']; } constructor() { // Toujours appeler "super" d'abord dans le constructeur super(); if (typeof this.internalStep == "undefined") { this.internalStep = 0; } this.img = []; this.maxStep = 0; this.figureName = this.getAttribute("figure-name"); this.promise = this.preloadImages(0).then(() => { this.updateSRC(); }); } preloadImage(i) { return new Promise((resolve, reject) => { this.img[i] = new Image(); this.img[i].onload = () => resolve(); this.img[i].onerror = reject; this.img[i].src = this.getURL(i); }); } preloadImages(i) { return new Promise((resolve, reject) => { this.preloadImage(i).then(() => { this.preloadImages(i + 1).then(() => { resolve(); }); }).catch(() => { this.maxStep = i - 1; resolve(); }); }); } connectedCallback() {} getURL(i) { return "figures/" + this.figureName + "/" + this.figureName + "_" + i + ".svg"; } updateSRC() { this.src = this.getURL(this.figureStep); } set figureStep(step) { this.promise = this.promise.then(() => { if (step > this.maxStep) this.internalStep = this.maxStep;else if (step < 0) this.internalStep = 0;else this.internalStep = step; this.updateSRC(); }); } get figureStep() { return this.internalStep; } attributeChangedCallback(name, oldValue, newValue) { if (name == "figure-name") { this.figureName = newValue; this.promise = this.promise.then(() => { this.preloadImages(0).then(() => { this.updateSRC(); }); }); } } nextFigure() { this.figuresStep++; } } customElements.define('slip-figure', SlipFigure, { extends: "img" }); let myQueryAll = (root, selector, avoid) => { avoid = avoid || "slip-slip"; if (!root.id) root.id = '_' + Math.random().toString(36).substr(2, 15); let allElem = Array.from(root.querySelectorAll(selector)); let separatedSelector = selector.split(",").map(selec => "#" + root.id + " " + avoid + " " + selec).join(); let other = Array.from(root.querySelectorAll(separatedSelector)); // let other = Array.from(root.querySelectorAll("#"+root.id+" " + avoid + " " + separatedSelector)); return allElem.filter(value => !other.includes(value)); }; window.myQueryAll = myQueryAll; function cloneNoSubslip(elem) { let newElem = elem.cloneNode(false); elem.childNodes.forEach(child => { if (child.tagName && child.tagName == "SLIP-SLIP") { let placeholder = document.createElement(child.tagName); let importantAttributes = ["pause", "step", "auto-enter", "immediate-enter"]; importantAttributes.forEach(attr => { if (child.hasAttribute(attr)) placeholder.setAttribute(attr, child.getAttribute(attr)); }); placeholder.classList.add("toReplace"); newElem.appendChild(placeholder); } else if (child.tagName && child.tagName == "CANVAS" && child.classList.contains("sketchpad")) { let placeholder = document.createElement(child.tagName); placeholder.classList.add("toReplaceSketchpad"); newElem.appendChild(placeholder); } else newElem.appendChild(cloneNoSubslip(child)); }); return newElem; } function replaceSubslips(clone, subslips, sketchpad, sketchpadHighlight) { let placeholders = myQueryAll(clone, ".toReplace"); subslips.forEach((subslip, index) => { let importantAttributes = ["pause", "step", "auto-enter", "immediate-enter"]; importantAttributes.forEach(attr => { if (placeholders[index].hasAttribute(attr)) subslip.setAttribute(attr, placeholders[index].getAttribute(attr)); }); placeholders[index].replaceWith(subslip); }); let sketchPlaceholder = myQueryAll(clone, ".toReplaceSketchpad"); if (sketchPlaceholder[0]) sketchPlaceholder[0].replaceWith(sketchpad); if (sketchPlaceholder[1]) sketchPlaceholder[1].replaceWith(sketchpadHighlight); } var IUtil = /*#__PURE__*/Object.freeze({ __proto__: null, cloneNoSubslip: cloneNoSubslip, myQueryAll: myQueryAll, replaceSubslips: replaceSubslips }); // import Hammer from 'hammerjs'; function IController (ng) { let engine = ng; this.getEngine = () => this.engine; this.setEngine = ng => this.engine = ng; let activated = true; this.activate = () => { activated = true; }; this.deactivate = () => { activated = false; }; let left_keys = ["k"], right_keys = ["m"], up_keys = ["o"], down_keys = ["l"], rotate_keys = ["i"], unrotate_keys = ["p"], zoom_keys = ["z"], unzoom_keys = ["Z"], show_toc_keys = ["T"], show_toc2_keys = ["t"], next_keys = ["ArrowRight", "ArrowDown"], previous_keys = ["ArrowLeft", "ArrowUp"], refresh_keys = ["r"], change_speed_keys = ["f"], up_slip_keys = [], draw_on_slip_keys = ["w"], erase_on_slip_keys = ["W"], highlight_on_slip_keys = ["h"], erase_highlight_on_slip_keys = ["H"], stop_writing_on_slip_keys = ["x"], clear_annotations_keys = ["X"], reload_canvas_keys = ["C"], background_canvas_keys = ["#"]; // let mainSlip = mainS; // this.getMainSlip = () => mainSlip; // this.setMainSlip = (slip) => mainSlip = slip; // let mc = new Hammer(document.body); // mc.on("swipe", (ev) => { // if (ev.direction == 2) { // engine.next(); // } // if (ev.direction == 4) { // engine.previous(); // } // }); let speedMove = 1; document.addEventListener("keypress", ev => { if (change_speed_keys.includes(ev.key) && activated) { speedMove = (speedMove + 4) % 30 + 1; } if (refresh_keys.includes(ev.key) && activated) { engine.getCurrentSlip().refresh(); } if (draw_on_slip_keys.includes(ev.key) && activated) { engine.setTool("drawing"); } if (erase_on_slip_keys.includes(ev.key) && activated) { engine.setTool("drawing-erase"); } if (highlight_on_slip_keys.includes(ev.key) && activated) { engine.setTool("highlighting"); } if (erase_highlight_on_slip_keys.includes(ev.key) && activated) { engine.setTool("highlighting-erase"); } if (stop_writing_on_slip_keys.includes(ev.key) && activated) { engine.setTool("no-tool"); } if (clear_annotations_keys.includes(ev.key) && activated) { engine.setTool("clear-all"); } if (reload_canvas_keys.includes(ev.key) && activated) { engine.reloadCanvas(); } if (background_canvas_keys.includes(ev.key) && activated) { document.querySelectorAll("slip-slip").forEach(slip => { slip.style.zIndex = "-1"; }); document.querySelectorAll(".background-canvas").forEach(canvas => { canvas.style.zIndex = "1"; }); } }); document.addEventListener("keydown", ev => { let openWindowHeight = engine.getOpenWindowHeight(); let openWindowWidth = engine.getOpenWindowWidth(); if (down_keys.includes(ev.key) && activated) { engine.moveWindowRelative(0, speedMove / openWindowHeight, 0, 0, 0.1); } // Bas if (up_keys.includes(ev.key) && activated) { engine.moveWindowRelative(0, -speedMove / openWindowHeight, 0, 0, 0.1); } // Haut if (left_keys.includes(ev.key) && activated) { engine.moveWindowRelative(-speedMove / openWindowWidth, 0, 0, 0, 0.1); } // Gauche if (right_keys.includes(ev.key) && activated) { engine.moveWindowRelative(speedMove / openWindowWidth, 0, 0, 0, 0.1); } // Droite if (rotate_keys.includes(ev.key) && activated) { engine.moveWindowRelative(0, 0, 0, 1, 0.1); } // Rotate if (unrotate_keys.includes(ev.key) && activated) { engine.moveWindowRelative(0, 0, 0, -1, 0.1); } // Unrotate if (zoom_keys.includes(ev.key) && activated) { engine.moveWindowRelative(0, 0, 0.01, 0, 0.1); } // Zoom if (unzoom_keys.includes(ev.key) && activated) { engine.moveWindowRelative(0, 0, -0.01, 0, 0.1); } // Unzoom if (show_toc_keys.includes(ev.key) && activated) { engine.showToC(); // document.querySelector(".toc-slip").style.display = document.querySelector(".toc-slip").style.display == "none" ? "block" : "none"; } if (show_toc2_keys.includes(ev.key) && activated) { // engine.showToC(); document.querySelector(".toc-slip").style.display = document.querySelector(".toc-slip").style.display == "none" ? "block" : "none"; } if (next_keys.includes(ev.key) && activated) { // console.log(ev); if (ev.shiftKey) engine.nextSlip();else engine.next(); } else if (previous_keys.includes(ev.key) && activated) { if (ev.shiftKey) engine.previousSlip();else engine.previous(); } else if (up_slip_keys.includes(ev.key) && activated) { engine.pop(); } }); } /* eslint-disable max-classes-per-file */ // make a class for Point class Point { constructor(x, y) { this.x = x; this.y = y; } set(x, y) { this.x = x; this.y = y; } } // make a class for the mouse data class Mouse extends Point { constructor() { super(0, 0); this.down = false; this.previous = new Point(0, 0); } } const floodFillInterval = 100; const maxLineThickness = 50; const minLineThickness = 1; const lineThicknessRange = maxLineThickness - minLineThickness; const thicknessIncrement = 0.5; const minSmoothingFactor = 0.87; const initialSmoothingFactor = 0.85; const weightSpread = 10; const initialThickness = 2; class AtramentEventTarget { constructor() { this.eventListeners = new Map(); } addEventListener(eventName, handler) { const handlers = this.eventListeners.get(eventName) || new Set(); handlers.add(handler); this.eventListeners.set(eventName, handlers); } removeEventListener(eventName, handler) { const handlers = this.eventListeners.get(eventName); if (!handlers) return; handlers.delete(handler); } dispatchEvent(eventName, data) { const handlers = this.eventListeners.get(eventName); if (!handlers) return; [...handlers].forEach(handler => handler(data)); } } const lineDistance = (x1, y1, x2, y2) => { // calculate euclidean distance between (x1, y1) and (x2, y2) const xs = (x2 - x1) ** 2; const ys = (y2 - y1) ** 2; return Math.sqrt(xs + ys); }; const hexToRgb = hexColor => { // Since input type color provides hex and ImageData accepts RGB need to transform const m = hexColor.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i); return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)]; }; const matchColor = (data, compR, compG, compB, compA) => pixelPos => { // Pixel color equals comp color? const r = data[pixelPos]; const g = data[pixelPos + 1]; const b = data[pixelPos + 2]; const a = data[pixelPos + 3]; return r === compR && g === compG && b === compB && a === compA; }; /* eslint-disable no-param-reassign */ const colorPixel = (data, fillR, fillG, fillB, startColor, alpha) => { const matcher = matchColor(data, ...startColor); return pixelPos => { // Update fill color in matrix data[pixelPos] = fillR; data[pixelPos + 1] = fillG; data[pixelPos + 2] = fillB; data[pixelPos + 3] = alpha; if (!matcher(pixelPos + 4)) { data[pixelPos + 4] = data[pixelPos + 4] * 0.01 + fillR * 0.99; data[pixelPos + 4 + 1] = data[pixelPos + 4 + 1] * 0.01 + fillG * 0.99; data[pixelPos + 4 + 2] = data[pixelPos + 4 + 2] * 0.01 + fillB * 0.99; data[pixelPos + 4 + 3] = data[pixelPos + 4 + 3] * 0.01 + alpha * 0.99; } if (!matcher(pixelPos - 4)) { data[pixelPos - 4] = data[pixelPos - 4] * 0.01 + fillR * 0.99; data[pixelPos - 4 + 1] = data[pixelPos - 4 + 1] * 0.01 + fillG * 0.99; data[pixelPos - 4 + 2] = data[pixelPos - 4 + 2] * 0.01 + fillB * 0.99; data[pixelPos - 4 + 3] = data[pixelPos - 4 + 3] * 0.01 + alpha * 0.99; } }; }; /* eslint-enable no-param-reassign */ const DrawingMode = { DRAW: 'draw', ERASE: 'erase', FILL: 'fill', DISABLED: 'disabled' }; const PathDrawingModes = [DrawingMode.DRAW, DrawingMode.ERASE]; class Atrament extends AtramentEventTarget { constructor(selector, config = {}) { if (typeof window === 'undefined') { throw new Error('Looks like we\'re not running in a browser'); } super(); // get canvas element if (selector instanceof window.Node && selector.tagName === 'CANVAS') this.canvas = selector;else if (typeof selector === 'string') this.canvas = document.querySelector(selector);else throw new Error(`can't look for canvas based on '${selector}'`); if (!this.canvas) throw new Error('canvas not found'); // set external canvas params this.canvas.width = config.width || this.canvas.width; this.canvas.height = config.height || this.canvas.height; // create a mouse object this.mouse = new Mouse(); // mousemove handler const mouseMove = event => { if (event.cancelable) { event.preventDefault(); } const rect = this.canvas.getBoundingClientRect(); const position = event.changedTouches && event.changedTouches[0] || event; let x = position.offsetX; let y = position.offsetY; if (typeof x === 'undefined') { x = position.clientX - rect.left; } if (typeof y === 'undefined') { y = position.clientY - rect.top; } const { mouse } = this; // draw if we should draw if (mouse.down && PathDrawingModes.includes(this.modeInternal)) { const { x: newX, y: newY } = this.draw(x, y, mouse.previous.x, mouse.previous.y); if (!this.dirty && this.modeInternal === DrawingMode.DRAW && (x !== mouse.x || y !== mouse.y)) { this.dirty = true; this.fireDirty(); } mouse.set(x, y); mouse.previous.set(newX, newY); } else { mouse.set(x, y); } }; // mousedown handler const mouseDown = event => { if (event.cancelable) { event.preventDefault(); } // update position just in case mouseMove(event); // if we are filling - fill and return if (this.mode === DrawingMode.FILL) { this.fill(); return; } // remember it const { mouse } = this; mouse.previous.set(mouse.x, mouse.y); mouse.down = true; this.beginStroke(mouse.previous.x, mouse.previous.y); }; const mouseUp = e => { if (this.mode === DrawingMode.FILL) { return; } const { mouse } = this; if (!mouse.down) { return; } const position = e.changedTouches && e.changedTouches[0] || e; const x = position.offsetX; const y = position.offsetY; mouse.down = false; if (mouse.x === x && mouse.y === y && PathDrawingModes.includes(this.mode)) { const { x: nx, y: ny } = this.draw(mouse.x, mouse.y, mouse.previous.x, mouse.previous.y); mouse.previous.set(nx, ny); } this.endStroke(mouse.x, mouse.y); }; // attach listeners this.canvas.addEventListener('mousemove', mouseMove); this.canvas.addEventListener('mousedown', mouseDown); document.addEventListener('mouseup', mouseUp); this.canvas.addEventListener('touchstart', mouseDown); this.canvas.addEventListener('touchend', mouseUp); this.canvas.addEventListener('touchmove', mouseMove); // helper for destroying Atrament (removing event listeners) this.destroy = () => { this.clear(); this.canvas.removeEventListener('mousemove', mouseMove); this.canvas.removeEventListener('mousedown', mouseDown); document.removeEventListener('mouseup', mouseUp); this.canvas.removeEventListener('touchstart', mouseDown); this.canvas.removeEventListener('touchend', mouseUp); this.canvas.removeEventListener('touchmove', mouseMove); }; // set internal canvas params this.context = this.canvas.getContext('2d'); this.context.globalCompositeOperation = 'source-over'; this.context.globalAlpha = 1; this.context.strokeStyle = config.color || 'rgba(0,0,0,1)'; this.context.lineCap = 'round'; this.context.lineJoin = 'round'; this.context.translate(0.5, 0.5); this.filling = false; this.fillStack = []; // set drawing params this.recordStrokes = false; this.strokeMemory = []; this.smoothing = initialSmoothingFactor; this.thickness = initialThickness; this.targetThickness = this.thickness; this.weightInternal = this.thickness; this.maxWeight = this.thickness + weightSpread; this.modeInternal = DrawingMode.DRAW; this.adaptiveStroke = true; // update from config object ['weight', 'smoothing', 'adaptiveStroke', 'mode'].forEach(key => { if (config[key] !== undefined) { this[key] = config[key]; } }); } /** * Begins a stroke at a given position * * @param {number} x * @param {number} y */ beginStroke(x, y) { this.context.beginPath(); this.context.moveTo(x, y); if (this.recordStrokes) { this.strokeTimestamp = performance.now(); this.strokeMemory.push({ point: new Point(x, y), time: performance.now() - this.strokeTimestamp }); } this.dispatchEvent('strokestart', { x, y }); } /** * Ends a stroke at a given position * * @param {number} x * @param {number} y */ endStroke(x, y) { this.context.closePath(); if (this.recordStrokes) { this.strokeMemory.push({ point: new Point(x, y), time: performance.now() - this.strokeTimestamp }); } this.dispatchEvent('strokeend', { x, y }); if (this.recordStrokes) { this.dispatchEvent('strokerecorded', { stroke: this.currentStroke }); } this.strokeMemory = []; delete this.strokeTimestamp; } /** * Draws a smooth quadratic curve with adaptive stroke thickness * between two points * * @param {number} x current X coordinate * @param {number} y current Y coordinate * @param {number} prevX previous X coordinate * @param {number} prevY previous Y coordinate */ draw(x, y, prevX, prevY) { if (this.recordStrokes) { this.strokeMemory.push({ point: new Point(x, y), time: performance.now() - this.strokeTimestamp }); this.dispatchEvent('pointdrawn', { stroke: this.currentStroke }); } const { context } = this; // calculate distance from previous point const rawDist = lineDistance(x, y, prevX, prevY); // now, here we scale the initial smoothing factor by the raw distance // this means that when the mouse moves fast, there is more smoothing // and when we're drawing small detailed stuff, we have more control // also we hard clip at 1 const smoothingFactor = Math.min(minSmoothingFactor, this.smoothing + (rawDist - 60) / 3000); // calculate processed coordinates const procX = x - (x - prevX) * smoothingFactor; const procY = y - (y - prevY) * smoothingFactor; // recalculate distance from previous point, this time relative to the smoothed coords const dist = lineDistance(procX, procY, prevX, prevY); if (this.adaptiveStroke) { // calculate target thickness based on the new distance this.targetThickness = (dist - minLineThickness) / lineThicknessRange * (this.maxWeight - this.weightInternal) + this.weightInternal; // approach the target gradually if (this.thickness > this.targetThickness) { this.thickness -= thicknessIncrement; } else if (this.thickness < this.targetThickness) { this.thickness += thicknessIncrement; } // set line width context.lineWidth = this.thickness; } else { // line width is equal to default weight context.lineWidth = this.weightInternal; } // draw using quad interpolation context.quadraticCurveTo(prevX, prevY, procX, procY); context.stroke(); return { x: procX, y: procY }; } get color() { return this.context.strokeStyle; } set color(c) { if (typeof c !== 'string') throw new Error('wrong argument type'); this.context.strokeStyle = c; } get weight() { return this.weightInternal; } set weight(w) { if (typeof w !== 'number') throw new Error('wrong argument type'); this.weightInternal = w; this.thickness = w; this.targetThickness = w; this.maxWeight = w + weightSpread; } get mode() { return this.modeInternal; } set mode(m) { if (typeof m !== 'string') throw new Error('wrong argument type'); switch (m) { case DrawingMode.ERASE: this.modeInternal = DrawingMode.ERASE; this.context.globalCompositeOperation = 'destination-out'; break; case DrawingMode.FILL: this.modeInternal = DrawingMode.FILL; this.context.globalCompositeOperation = 'source-over'; break; case DrawingMode.DISABLED: this.modeInternal = DrawingMode.DISABLED; break; default: this.modeInternal = DrawingMode.DRAW; this.context.globalCompositeOperation = 'source-over'; break; } } get currentStroke() { return { points: this.strokeMemory.slice(), mode: this.mode, weight: this.weight, smoothing: this.smoothing, color: this.color, adaptiveStroke: this.adaptiveStroke }; } isDirty() { return !!this.dirty; } fireDirty() { this.dispatchEvent('dirty'); } clear() { if (!this.isDirty) { return; } this.dirty = false; this.dispatchEvent('clean'); // make sure we're in the right compositing mode, and erase everything if (this.modeInternal === DrawingMode.ERASE) { this.modeInternal = DrawingMode.DRAW; this.context.clearRect(-10, -10, this.canvas.width + 20, this.canvas.height + 20); this.modeInternal = DrawingMode.ERASE; } else { this.context.clearRect(-10, -10, this.canvas.width + 20, this.canvas.height + 20); } } toImage() { return this.canvas.toDataURL(); } fill() { const { mouse } = this; const { context } = this; // converting to Array because Safari 9 const startColor = Array.from(context.getImageData(mouse.x, mouse.y, 1, 1).data); if (!this.filling) { const { x, y } = mouse; this.dispatchEvent('fillstart', { x, y }); this.filling = true; setTimeout(() => { this.floodFill(mouse.x, mouse.y, startColor); }, floodFillInterval); } else { this.fillStack.push([mouse.x, mouse.y, startColor]); } } floodFill(_startX, _startY, startColor) { const { context } = this; const startX = Math.floor(_startX); const startY = Math.floor(_startY); const canvasWidth = context.canvas.width; const canvasHeight = context.canvas.height; const pixelStack = [[startX, startY]]; // hex needs to be trasformed to rgb since colorLayer accepts RGB const fillColor = hexToRgb(this.color); // Need to save current context with colors, we will update it const colorLayer = context.getImageData(0, 0, context.canvas.width, context.canvas.height); const alpha = Math.min(context.globalAlpha * 10 * 255, 255); const colorPixel$1 = colorPixel(colorLayer.data, ...fillColor, startColor, alpha); const matchColor$1 = matchColor(colorLayer.data, ...startColor); const matchFillColor = matchColor(colorLayer.data, ...[...fillColor, 255]); // check if we're trying to fill with the same colour, if so, stop if (matchFillColor((startY * context.canvas.width + startX) * 4)) { this.filling = false; this.dispatchEvent('fillend', {}); return; } while (pixelStack.length) { const newPos = pixelStack.pop(); const x = newPos[0]; let y = newPos[1]; let pixelPos = (y * canvasWidth + x) * 4; while (y-- >= 0 && matchColor$1(pixelPos)) { pixelPos -= canvasWidth * 4; } pixelPos += canvasWidth * 4; ++y; let reachLeft = false; let reachRight = false; while (y++ < canvasHeight - 1 && matchColor$1(pixelPos)) { colorPixel$1(pixelPos); if (x > 0) { if (matchColor$1(pixelPos - 4)) { if (!reachLeft) { pixelStack.push([x - 1, y]); reachLeft = true; } } else if (reachLeft) { reachLeft = false; } } if (x < canvasWidth - 1) { if (matchColor$1(pixelPos + 4)) { if (!reachRight) { pixelStack.push([x + 1, y]); reachRight = true; } } else if (reachRight) { reachRight = false; } } pixelPos += canvasWidth * 4; } } // Update context with filled bucket! context.putImageData(colorLayer, 0, 0); if (this.fillStack.length) { this.floodFill(...this.fillStack.shift()); } else { this.filling = false; this.dispatchEvent('fillend', {}); } } } function Slip$1(name, fullName, actionL, ng, options) { // ****************************** // Action List // ****************************** this.generateActionList = function () { let newActionList = []; this.queryAll("slip-slip[enter-at]").forEach(slip => { newActionList[slip.getAttribute("enter-at")] = new Slip$1(slip, "", [], ng, {}); }); return newActionList; }; this.addSubSlips = function () { let newActionList = []; this.queryAll("slip-slip[enter-at]").forEach(slip => { this.setNthAction(slip.getAttribute("enter-at"), new Slip$1(slip, "", [], ng, {})); }); return newActionList; }; let actionList = actionL; // || this.generateActionList(); this.setAction = actionL => { actionList = actionL; }; this.getActionList = () => { let ret = []; for (let i = 0; i <= this.getMaxNext(); i++) { if (this.pauseSlipList[i] instanceof Slip$1) ret[i] = this.pauseSlipList[i];else if (typeof actionList[i] == "function" || actionList[i] instanceof Slip$1) ret[i] = actionList[i];else ret[i] = () => {}; } return ret; }; this.setNthAction = (n, action) => { actionList[n] = action; }; this.getCurrentSubSlip = () => { if (actionList[this.getActionIndex()] instanceof Slip$1) return actionList[this.getActionIndex()]; if (this.pauseSlipList[this.getActionIndex()] instanceof Slip$1) return this.pauseSlipList[this.getActionIndex()]; return false; }; this.nextStageNeedGoto = () => { if (actionList[this.getActionIndex() + 1] instanceof Slip$1) return false; if (this.pauseSlipList[this.getActionIndex() + 1] instanceof Slip$1) return false; if (this.getActionIndex() >= this.getMaxNext()) return false; return true; }; this.getSubSlipList = function () { return this.getActionList().filter(action => action instanceof Slip$1); }; // ****************************** // Action Index // ****************************** let actionIndex = -1; this.setActionIndex = actionI => actionIndex = actionI; this.getActionIndex = () => actionIndex; this.getMaxNext = () => { if (this.maxNext) return this.maxNext; let maxTemp = actionList.length; ["mk-visible-at", "mk-hidden-at", "mk-emphasize-at", "mk-unemphasize-at", "emphasize-at", "chg-visib-at", "up-at", "down-at", "center-at", "static-at", "exec-at", "enter-at", "focus-at", "unfocus-at", "figure-next-at", "figure-previous-at"].forEach(attr => { this.queryAll("*[" + attr + "]").forEach(elem => { elem.getAttribute(attr).split(" ").forEach(strMax => { maxTemp = Math.max(Math.abs(parseInt(strMax)), maxTemp); }); }); }); let sumArray = this.queryAll("[pause], [step], [auto-enter], [immediate-enter]").map(elem => { if (elem.hasAttribute("pause") && elem.getAttribute("pause") != "") return parseInt(elem.getAttribute("pause")); if (elem.hasAttribute("step") && elem.getAttribute("step") != "") return parseInt(elem.getAttribute("step")); return 1; }); maxTemp = Math.max(maxTemp, sumArray.reduce((a, b) => a + b, 0)); this.maxNext = maxTemp; return maxTemp; }; // ****************************** // Queries // ****************************** this.queryAll = quer => { return myQueryAll(this.element, quer); // let allElem = Array.from(this.element.querySelectorAll(quer)); // let other = Array.from(this.element.querySelectorAll("#"+this.name+" slip "+quer)); // return allElem.filter(value => !other.includes(value)); }; this.query = quer => { if (typeof quer != "string") return quer; return this.queryAll(quer)[0]; }; this.findSubslipByID = id => { let goodSubslip = this.getSubSlipList().find(subslip => { if (subslip.name == id) return 1; return subslip.findSubslipByID(id); }); if (!goodSubslip) return false; if (goodSubslip.name == id) return goodSubslip; return goodSubslip.findSubslipByID(id); }; // ****************************** // Coordinates // ****************************** this.findSlipCoordinate = () => { // rename to getCoordInUniverse let coord = engine.getCoordinateInUniverse(this.element); coord.scale *= this.scale; coord.y = coord.y + 0.5 * coord.scale; coord.x = coord.centerX; return coord; }; // ****************************** // Pause functions // ****************************** this.updatePauseAncestors = () => { this.queryAll(".pauseAncestor").forEach(elem => { elem.classList.remove("pauseAncestor"); }); let pause = this.query("[pause]"); while (pause && pause.tagName != "SLIP-SLIP") { pause.classList.add("pauseAncestor"); pause = pause.parentElement; } }; this.unpause = pause => { if (pause.hasAttribute("static-at-unpause")) { if (pause.getAttribute("static-at-unpause") == "") this.makeStatic(pause);else pause.getAttribute("static-at-unpause").split(" ").map(strID => { this.makeStatic("#" + strID); }); } if (pause.hasAttribute("unstatic-at-unpause")) { if (pause.getAttribute("unstatic-at-unpause") == "") this.makeUnStatic(pause);else pause.getAttribute("unstatic-at-unpause").split(" ").map(strID => { this.makeUnStatic("#" + strID); }); } if (pause.hasAttribute("down-at-unpause")) { if (pause.getAttribute("down-at-unpause") == "") this.moveDownTo(pause, 1);else this.moveDownTo("#" + pause.getAttribute("down-at-unpause"), 1); } if (pause.hasAttribute("up-at-unpause")) { if (pause.getAttribute("up-at-unpause") == "") this.moveUpTo(pause, 1);else this.moveUpTo("#" + pause.getAttribute("up-at-unpause"), 1); } if (pause.hasAttribute("center-at-unpause")) { if (pause.getAttribute("center-at-unpause") == "") this.moveCenterTo(pause, 1);else this.moveCenterTo("#" + pause.getAttribute("center-at-unpause"), 1); } if (pause.hasAttribute("exec-at-unpause")) { if (pause.getAttribute("exec-at-unpause") == "") this.executeScript(pause);else pause.getAttribute("exec-at-unpause").split(" ").map(strID => { this.executeScript("#" + strID); }); } if (pause.hasAttribute("reveal-at-unpause")) { if (pause.getAttribute("reveal-at-unpause") == "") this.reveal(pause);else pause.getAttribute("reveal-at-unpause").split(" ").map(strID => { this.reveal("#" + strID); }); } if (pause.hasAttribute("hide-at-unpause")) { if (pause.getAttribute("hide-at-unpause") == "") this.hide(pause);else pause.getAttribute("hide-at-unpause").split(" ").map(strID => { this.hide("#" + strID); }); } if (pause.hasAttribute("figure-set-at-unpause")) { let [figureID, figureStep] = pause.getAttribute("figure-set-at-unpause").split(" "); this.query("#" + figureID).figureStep = figureStep; } if (pause.hasAttribute("figure-next-at-unpause")) { pause.getAttribute("figure-next-at-unpause").split(" ").map(figureID => { this.query("#" + figureID).figureStep++; }); } if (pause.hasAttribute("figure-previous-at-unpause")) { pause.getAttribute("figure-previous-at-unpause").split(" ").map(figureID => { this.query("#" + figureID).figureStep--; }); } if (pause.hasAttribute("focus-at-unpause")) { if (pause.getAttribute("focus-at-unpause") == "") this.focus(pause);else this.focus("#" + pause.getAttribute("focus-at-unpause")); } if (pause.hasAttribute("emph-at-unpause")) { if (pause.getAttribute("emph-at-unpause") == "") this.makeEmph(pause);else pause.getAttribute("emph-at-unpause").split(" ").map(strID => { this.makeEmph("#" + strID); }); } if (pause.hasAttribute("unemph-at-unpause")) { if (pause.getAttribute("unemph-at-unpause") == "") this.makeUnEmph(pause);else pause.getAttribute("unemph-at-unpause").split(" ").map(strID => { this.makeUnEmph("#" + strID); }); } if (pause.hasAttribute("unfocus-at-unpause")) { if (pause.getAttribute("unfocus-at-unpause") == "") this.unfocus(pause);else this.unfocus("#" + pause.getAttribute("unfocus-at-unpause")); } }; this.incrPause = () => { let pause = this.query("[pause], [auto-enter]:not([auto-enter=\"0\"]), [immediate-enter]:not([immediate-enter=\"0\"]), [step]"); // let pause = this.query("[pause]"); if (pause) { if (pause.hasAttribute("step")) { if (!pause.getAttribute("step")) pause.setAttribute("step", 1); let d = pause.getAttribute("step"); if (d <= 1) { pause.removeAttribute("step"); this.unpause(pause); } else pause.setAttribute("step", d - 1); } if (pause.hasAttribute("auto-enter")) { pause.setAttribute("auto-enter", 0); this.unpause(pause); } if (pause.hasAttribute("immediate-enter")) { pause.setAttribute("immediate-enter", 0); this.unpause(pause); } if (pause.hasAttribute("pause")) { if (!pause.getAttribute("pause")) pause.setAttribute("pause", 1); let d = pause.getAttribute("pause"); if (d <= 1) { pause.removeAttribute("pause"); this.unpause(pause); } else pause.setAttribute("pause", d - 1); this.updatePauseAncestors(); } } }; // ****************************** // Next functions // ****************************** this.doAttributes = () => { this.queryAll("*[mk-hidden-at]").forEach(elem => { let hiddenAt = elem.getAttribute("mk-hidden-at").split(" ").map(str => parseInt(str)); if (hiddenAt.includes(actionIndex)) elem.style.opacity = "0"; }); this.queryAll("*[mk-visible-at]").forEach(elem => { let visibleAt = elem.getAttribute("mk-visible-at").split(" ").map(str => parseInt(str)); if (visibleAt.includes(actionIndex)) elem.style.opacity = "1"; }); this.queryAll("*[mk-emphasize-at]").forEach(elem => { let emphAt = elem.getAttribute("mk-emphasize-at").split(" ").map(str => parseInt(str)); if (emphAt.includes(actionIndex)) elem.classList.add("emphasize"); }); this.queryAll("*[mk-unemphasize-at]").forEach(elem => { let unemphAt = elem.getAttribute("mk-unemphasize-at").split(" ").map(str => parseInt(str)); if (unemphAt.includes(actionIndex)) elem.classList.remove("emphasize"); }); this.queryAll("*[emphasize-at]").forEach(elem => { let emphAt = elem.getAttribute("emphasize-at").split(" ").map(str => parseInt(str)); if (emphAt.includes(actionIndex)) elem.classList.add("emphasize");else elem.classList.remove("emphasize"); }); this.queryAll("*[chg-visib-at]").forEach(elem => { let visibAt = elem.getAttribute("chg-visib-at").split(" ").map(str => parseInt(str)); if (visibAt.includes(actionIndex)) elem.style.opacity = "1"; if (visibAt.includes(-actionIndex)) elem.style.opacity = "0"; }); this.queryAll("*[static-at]").forEach(elem => { let staticAt = elem.getAttribute("static-at").split(" ").map(str => parseInt(str)); if (actionIndex < 0) return; if (staticAt.includes(-actionIndex)) { this.makeUnStatic(elem); // elem.style.position = "absolute"; // elem.style.visibility = "hidden"; } else if (staticAt.includes(actionIndex)) { this.makeStatic(elem); // elem.style.position = "static"; // elem.style.visibility = "visible"; } }); this.queryAll("*[down-at]").forEach(elem => { let goDownTo = elem.getAttribute("down-at").split(" ").map(str => parseInt(str)); if (goDownTo.includes(actionIndex)) this.moveDownTo(elem, 1); }); this.queryAll("*[up-at]").forEach(elem => { let goTo = elem.getAttribute("up-at").split(" ").map(str => parseInt(str)); if (goTo.includes(actionIndex)) this.moveUpTo(elem, 1); }); this.queryAll("*[center-at]").forEach(elem => { let goDownTo = elem.getAttribute("center-at").split(" ").map(str => parseInt(str)); if (goDownTo.includes(actionIndex)) this.moveCenterTo(elem, 1); }); this.queryAll("*[focus-at]").forEach(elem => { let focus = elem.getAttribute("focus-at").split(" ").map(str => parseInt(str)); if (focus.includes(actionIndex)) this.focus(elem, 1); }); this.queryAll("*[unfocus-at]").forEach(elem => { let focus = elem.getAttribute("unfocus-at").split(" ").map(str => parseInt(str)); if (focus.includes(actionIndex)) this.unfocus(elem, 1); }); this.queryAll("*[exec-at]").forEach(elem => { let toExec = elem.getAttribute("exec-at").split(" ").map(str => parseInt(str)); if (toExec.includes(actionIndex)) this.executeScript(elem); }); this.queryAll("*[figure-next-at]").forEach(elem => { let toFigureNext = elem.getAttribute("figure-next-at").split(" ").map(str => parseInt(str)); if (toFigureNext.includes(actionIndex)) elem.figureStep++; }); this.queryAll("*[figure-previous-at]").forEach(elem => { let toFigureNext = elem.getAttribute("figure-previous-at").split(" ").map(str => parseInt(str)); if (toFigureNext.includes(actionIndex)) elem.figureStep--; }); }; this.incrIndex = () => { actionIndex = actionIndex + 1; this.doAttributes(); if (actionIndex > 0) this.incrPause(); this.updateToC(); }; this.next = function () { if (actionIndex >= this.getMaxNext()) return false; this.incrIndex(); if (typeof actionList[actionIndex] == "function") { actionList[actionIndex](this); } if (actionList[actionIndex] instanceof Slip$1) { return actionList[actionIndex]; } if (this.pauseSlipList[actionIndex] instanceof Slip$1) return this.pauseSlipList[actionIndex]; // let nextSlip = this.query("[pause], [auto-enter]"); // if(nextSlip.hasAttribute("auto-enter")) // return return true; }; this.previous = () => { let savedActionIndex = this.getActionIndex(); this.currentDelay; this.getEngine().setDoNotMove(true); let savedClass = this.element.className; this.doRefresh(); this.element.className = savedClass; if (savedActionIndex == -1) return false; let toReturn; while (this.getActionIndex() < savedActionIndex - 1) { toReturn = this.next(); } // if(!this.nextStageNeedGoto()) // this.getEngine().setDoNotMove(false); // while(this.getActionIndex()<savedActionIndex-1) // toReturn = this.next(); setTimeout(() => { this.getEngine().setDoNotMove(false); // this.getEngine().gotoSlip(this, {delay:savedDelay}); }, 0); // this.getEngine().gotoSlip(this, {delay:savedDelay}); return toReturn; // return this.next; }; // ****************************** // ToC functions // ****************************** this.setTocElem = tocElem => { this.tocElem = tocElem; }; this.updateToC = () => { if (!this.tocElem) return; if (!this.ToCList) this.ToCList = myQueryAll(this.tocElem, "li", "li"); let i; for (i = 0; i < this.getActionIndex(); i++) { this.ToCList[i].classList.remove("before", "after", "current"); this.ToCList[i].classList.add("before"); } if (i <= this.getActionIndex()) { this.ToCList[i].classList.remove("before", "after", "current"); this.ToCList[i].classList.add("current"); i++; } for (i; i <= this.getMaxNext(); i++) { this.ToCList[i].classList.remove("before", "after", "current"); this.ToCList[i].classList.add("after"); } }; this.firstVisit = () => { this.updateToC(); if (options.firstVisit) options.firstVisit(this); }; this.init = () => { this.queryAll("*[chg-visib-at]").forEach(elem => { elem.style.opacity = "0"; }); // this.queryAll("*[static-at]").forEach((elem) => { // elem.style.position = "absolute"; // elem.style.visibility = "hidden"; // }); // this.doAttributes(); this.updatePauseAncestors(); if (options.init) options.init(this); }; // ****************************** // Refreshes // ****************************** this.refresh = () => { if (actionList[actionIndex] instanceof Slip$1) actionList[actionIndex].refresh();else this.doRefresh(); }; this.refreshAll = () => { actionList.filter(elem => elem instanceof Slip$1).forEach(subslip => { subslip.refreshAll(); }); this.pauseSlipList.filter(elem => elem instanceof Slip$1).forEach(subslip => { subslip.refreshAll(); }); this.doRefresh(); }; this.doRefresh = () => { this.setActionIndex(-1); let subSlipList = myQueryAll(this.element, "slip-slip"); let clone = clonedElement.cloneNode(true); replaceSubslips(clone, subSlipList, this.sketchpadCanvas, this.sketchpadCanvasHighlight); this.element.replaceWith(clone); this.element = clone; this.init(); this.firstVisit(); delete this.currentX; delete this.currentY; delete this.currentDelay; this.getEngine().gotoSlip(this); setTimeout(() => { this.reObserve(); }, 0); }; this.reObserve = () => { let slipScaleContainer = this.element.firstChild; if (slipScaleContainer && slipScaleContainer.classList && slipScaleContainer.classList.contains("slip-scale-container")) this.resizeObserver.observe(slipScaleContainer); this.getSubSlipList().forEach(slip => { slip.reObserve(); }); }; // ****************************** // Movement, execution and hide/show // ****************************** this.makeUnStatic = (selector, delay, opacity) => { let elem = this.query(selector); // setTimeout(() => { // elem.style.overflow = "hidden"; // setTimeout(() => { // elem.style.transition = "height "+ (typeof(delay) == "undefined" ? "1s" : (delay+"s")); // if(opacity) // elem.style.transition += ", opacity "+ (typeof(delay) == "undefined" ? "1s" : (delay+"s")); // elem.style.height = (elem.offsetHeight+"px"); // if(opacity) // elem.style.opacity = "1"; // setTimeout(() => { // if(opacity) // elem.style.opacity = "0"; // elem.style.height = ("0px");}, 10); // }, 0); // },0); elem.style.position = "absolute"; elem.style.visibility = "hidden"; }; this.makeStatic = selector => { let elem = this.query(selector); elem.style.position = "static"; elem.style.visibility = "visible"; }; this.makeEmph = selector => { let elem = this.query(selector); elem.classList.add("emphasize"); }; this.makeUnEmph = selector => { let elem = this.query(selector); elem.classList.remove("emphasize"); }; this.unfocus = selector => { this.getEngine().gotoSlip(this, { delay: 1 }); }; this.focus = selector => { let elem = this.query(selector); this.getEngine().moveToElement(elem, {}); }; this.executeScript = selector => { let elem; if (typeof selector == "string") elem = this.query(selector);else elem = selector; new Function("slip", elem.innerHTML)(this); }; this.moveUpTo = (selector, delay, offset) => { setTimeout(() => { let elem; if (typeof selector == "string") elem = this.query(selector);else elem = selector; if (typeof offset == "undefined") offset = 0.0125; let coord = this.findSlipCoordinate(); let d = (elem.offsetTop / 1080 - offset) * coord.scale; this.moveWindow(coord.x, coord.y + d, coord.scale, this.rotate, delay); // this.currentX = coord.x; // this.currentY = coord.y+d; // this.currentDelay = delay; // engine.moveWindow(coord.x, coord.y+d, coord.scale, this.rotate, delay); }, 0); }; this.moveDownTo = (selector, delay, offset) => { setTimeout(() => { let elem; if (typeof selector == "string") elem = this.query(selector);else elem = selector; if (typeof offset == "undefined") offset = 0.0125; let coord = this.findSlipCoordinate(); let d = ((elem.offsetTop + elem.offsetHeight) / 1080 - 1 + offset) * coord.scale; this.moveWindow(coord.x, coord.y + d, coord.scale, this.rotate, delay); // this.currentX = coord.x; // this.currentY = coord.y+d; // this.currentDelay = delay; // engine.moveWindow(coord.x, coord.y+d, coord.scale, this.rotate, delay); }, 0); }; this.moveCenterTo = (selector, delay, offset) => { setTimeout(() => { let elem; if (typeof selector == "string") elem = this.query(selector);else elem = selector; if (typeof offset == "undefined") offset = 0; let coord = this.findSlipCoordinate(); let d = ((elem.offsetTop + elem.offsetHeight / 2) / 1080 - 1 / 2 + offset) * coord.scale; this.moveWindow(coord.x, coord.y + d, coord.scale, this.rotate, delay); // this.currentX = coord.x; // this.currentY = coord.y+d; // this.currentDelay = delay; // engine.moveWindow(coord.x, coord.y+d, coord.scale, this.rotate, delay); }, 0); }; this.restoreWindow = () => { this.getEngine; }; this.moveWindow = (x, y, scale, rotate, delay) => { this.currentX = x; this.currentY = y; this.currentScale = scale; this.currentDelay = delay; // setTimeout(() => { this.getEngine().moveWindow(x, y, scale, rotate, delay); // }, 0); }; this.reveal = selector => { let elem; if (typeof selector == "string") elem = this.query(selector);else elem = selector; elem.style.opacity = "1"; }; this.revealAll = selector => { this.queryAll(selector).forEach(elem => { elem.style.opacity = "1"; }); }; this.hide = selector => { this.query(selector).style.opacity = "0"; }; this.hideAll = selector => { this.queryAll(selector).forEach(elem => { elem.style.opacity = "0"; }); }; // ****************************** // Function for writing and highlighting // ****************************** this.tool = "no-tool"; this.getTool = tool => { return this.tool; }; this.setTool = tool => { if (tool == "clear-all") { this.sketchpad.clear(); this.sketchpadHighlight.clear(); return; } this.tool = tool; this.element.classList.remove("drawing", "highlighting"); if (tool == "highlighting") { this.element.classList.add("highlighting"); this.sketchpadHighlight.mode = "draw"; } else if (tool == "highlighting-erase") { this.element.classList.add("highlighting"); this.sketchpadHighlight.mode = "erase"; } else if (tool == "drawing") { this.element.classList.add("drawing"); this.sketchpad.mode = "draw"; this.sketchpad.weight = 1; } else if (tool == "drawing-erase") { this.element.classList.add("drawing"); this.sketchpad.weight = 20; this.sketchpad.mode = "erase"; } else ; }; this.color = "blue"; this.colorHighlight = "yellow"; this.getColor = () => { if (["highlighting", "highlighting-erase"].includes(this.getTool())) { return this.colorHighlight; } else if (["drawing", "drawing-erase"].includes(this.getTool())) { return th