artboard-deluxe
Version:
Touch-friendly draggable artboard
1,699 lines (1,684 loc) • 85.7 kB
JavaScript
'use strict';
function adjustScaleForPrecision(size, currentScale, precision) {
const scaledSize = size * currentScale;
const targetSize = Math.round(scaledSize / precision) * precision;
return targetSize / size;
}
function dampenRelative(value, min, max, factor) {
if (value < min) {
const overshoot = value - min;
return min + overshoot * factor / (1 + Math.abs(overshoot) / (max - min));
} else if (value > max) {
const overshoot = value - max;
return max + overshoot * factor / (1 + Math.abs(overshoot) / (max - min));
}
return value;
}
function dampen(value, min, max, factor) {
if (value < min) {
const overshoot = value - min;
return min + overshoot * factor;
} else if (value > max) {
const overshoot = value - max;
return max + overshoot * factor;
}
return value;
}
function lerp(s, e, t) {
return s * (1 - t) + e * t;
}
function clamp(a, min = 0, max = 1) {
return Math.min(max, Math.max(min, a));
}
function calculateCenterPosition(blockingRects, viewport, widthToPlace) {
const viewportCenterX = (viewport.x + viewport.width) / 2;
const blockingThreshold = viewport.width / 7;
const x = blockingRects.reduce((acc, rect) => {
if (rect.x < viewportCenterX && viewportCenterX - rect.x > blockingThreshold && rect.x + rect.width > acc) {
return rect.x + rect.width;
}
return acc;
}, viewport.x);
const availableWidth = blockingRects.reduce((acc, rect) => {
if (rect.x > viewportCenterX && rect.x - viewportCenterX > blockingThreshold && rect.x < acc) {
return rect.x;
}
return acc;
}, viewport.width + viewport.x);
const centerX = (x + availableWidth) / 2 - widthToPlace / 2 - viewport.x;
return { centerX, availableWidth: availableWidth - x };
}
function getMidpoint(touches) {
const x = touches[0].clientX;
const y = touches[0].clientY;
if (touches.length === 1) {
return {
x,
y
};
}
return {
x: (x + touches[1].clientX) / 2,
y: (y + touches[1].clientY) / 2
};
}
function getDistance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
function getTouchDistance(touches) {
return getDistance(
{
x: touches[0].clientX,
y: touches[0].clientY
},
{
x: touches[1].clientX,
y: touches[1].clientY
}
);
}
function limitOffset(x, y, boundaries) {
return {
x: clamp(x, boundaries.xMin, boundaries.xMax),
y: clamp(y, boundaries.yMin, boundaries.yMax)
};
}
function getDirection(a, b, threshold) {
const angle = Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI;
const isHorizontal = angle >= -threshold && angle <= threshold || angle >= 180 - threshold || angle <= -180 + threshold;
const isVertical = angle >= 90 - threshold && angle <= 90 + threshold || angle >= -90 - threshold && angle <= -90 + threshold;
if (isHorizontal && !isVertical) {
return "horizontal";
} else if (!isHorizontal && isVertical) {
return "vertical";
}
return "both";
}
function getEventCoords(e) {
if (isTouchEvent(e)) {
return {
x: e.touches[0].pageX,
y: e.touches[0].pageY
};
}
return {
x: e.pageX,
y: e.pageY
};
}
function isTouchEvent(e) {
return window.TouchEvent && e instanceof TouchEvent;
}
function withPrecision(value, precision) {
return Math.ceil(value / precision) * precision;
}
function parseOrigin(origin) {
const [vertical, horizontal] = origin.split("-");
const x = horizontal === "left" ? 0 : horizontal === "center" ? 0.5 : (
/* 'right' */
1
);
const y = vertical === "top" ? 0 : vertical === "center" ? 0.5 : (
/* 'bottom' */
1
);
return { x, y };
}
function withDefault(v, defaultValue) {
if (v === void 0 || v === null) {
return defaultValue;
}
return Number.isNaN(v) ? defaultValue : v;
}
function parseEdges(v, defaultValue = 0) {
if (typeof v === "number") {
return {
top: v,
right: v,
bottom: v,
left: v
};
}
if (!v) {
return {
top: defaultValue,
right: defaultValue,
bottom: defaultValue,
left: defaultValue
};
}
return {
top: withDefault(v.top, defaultValue),
right: withDefault(v.right, defaultValue),
bottom: withDefault(v.bottom, defaultValue),
left: withDefault(v.left, defaultValue)
};
}
function asValidNumber(value, defaultValue = 0) {
if (value === null || value === void 0) {
return defaultValue;
}
if (typeof value === "string") {
return asValidNumber(parseFloat(value));
}
if (Number.isNaN(value)) {
return defaultValue;
}
return value;
}
function easeOutQuad(x) {
return 1 - (1 - x) * (1 - x);
}
function easeOutBack(x) {
const c1 = 1.70158;
const c3 = c1 + 1;
return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
}
function easeOutElastic(x) {
const c4 = 2 * Math.PI / 3;
if (x === 0) {
return 0;
} else if (x === 1) {
return 1;
}
return Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
}
function easeOutCirc(x) {
return Math.sqrt(1 - Math.pow(x - 1, 2));
}
function easeInOutExpo(x) {
if (x === 0) {
return 0;
} else if (x === 1) {
return 1;
}
return x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2;
}
function easeOutCubic(x) {
return 1 - Math.pow(1 - x, 3);
}
function easeInSine(x) {
return 1 - Math.cos(x * Math.PI / 2);
}
function easeInOutSine(x) {
return -(Math.cos(Math.PI * x) - 1) / 2;
}
function easeInOutQuad(x) {
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
}
function easeInOutQuart(x) {
return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2;
}
function easeInOutCirc(x) {
return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2;
}
function easeInOutQuint(x) {
return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
}
function linear(x) {
return x;
}
const EASINGS = {
easeInOutExpo,
easeOutCirc,
easeOutElastic,
easeOutQuad,
easeOutBack,
easeOutCubic,
easeInSine,
easeInOutSine,
easeInOutQuad,
easeInOutQuart,
easeInOutCirc,
easeInOutQuint,
linear
};
function applyAnimation(state, currentTime, animation) {
if (!animation.startTime) {
animation.startTime = currentTime;
}
const elapsedTime = currentTime - animation.startTime;
const progress = Math.min(elapsedTime / animation.duration, 1);
const easedProgress = typeof animation.easing === "string" ? EASINGS[animation.easing](progress) : animation.easing(progress);
const x = lerp(animation.startX, animation.x, easedProgress);
const y = lerp(animation.startY, animation.y, easedProgress);
state.offset.x = x;
state.offset.y = y;
state.scale = lerp(animation.startScale, animation.scale, easedProgress);
if (progress >= 1) {
state.offset.x = animation.x;
state.offset.y = animation.y;
state.scale = animation.scale;
return true;
}
return false;
}
function applyMomentum(state, currentTime, boundaries) {
if (state.momentum === null) {
return true;
}
const deltaTime = (currentTime - state.lastLoopTimestamp) / 1e3;
if (deltaTime > 0.5) {
state.momentum = null;
return true;
}
const inLeft = state.offset.x - boundaries.xMin >= -1.5;
const inRight = state.offset.x - boundaries.xMax <= 1.5;
const inTop = state.offset.y - boundaries.yMin >= -1.5;
const inBottom = state.offset.y - boundaries.yMax <= 1.5;
const deceleration = state.momentum.deceleration;
const appliedDecelerationX = inLeft && inRight ? deceleration : 0.85;
const appliedDecelerationY = inTop && inBottom ? deceleration : 0.85;
state.momentum.x *= appliedDecelerationX;
state.momentum.y *= appliedDecelerationY;
const vxDone = Math.abs(state.momentum.x) < 2;
const vyDone = Math.abs(state.momentum.y) < 2;
if (!vxDone) {
state.offset.x = dampen(
state.offset.x + state.momentum.x * deltaTime,
boundaries.xMin,
boundaries.xMax,
appliedDecelerationX
);
}
if (!vyDone) {
state.offset.y = dampen(
state.offset.y + state.momentum.y * deltaTime,
boundaries.yMin,
boundaries.yMax,
appliedDecelerationY
);
}
return vxDone && vyDone && inLeft && inRight && inTop && inBottom;
}
function applyScaleMomentum(state, currentTime, minScale, maxScale) {
const deceleration = 0.09;
const minThreshold = 0.01;
if (!state.scaleVelocity) {
return true;
}
const deltaTime = (currentTime - state.lastLoopTimestamp) / 1e3;
if (deltaTime > 0.5) {
state.scaleVelocity = null;
return true;
}
if (state.scaleVelocity.scale > maxScale) {
state.scaleVelocity.scale = state.scaleVelocity.scale * 0.95;
}
state.offset.x = lerp(state.offset.x, state.scaleVelocity.x, deceleration);
state.offset.y = lerp(state.offset.y, state.scaleVelocity.y, deceleration);
state.scale = lerp(state.scale, state.scaleVelocity.scale, deceleration);
const vxDone = Math.abs(state.scaleVelocity.x - state.offset.x) < minThreshold;
const vyDone = Math.abs(state.scaleVelocity.y - state.offset.y) < minThreshold;
const scaleDone = Math.abs(state.scaleVelocity.scale - state.scale) < minThreshold;
const isDone = vxDone && vyDone && scaleDone;
if (isDone) {
state.offset.x = state.scaleVelocity.x;
state.offset.y = state.scaleVelocity.y;
state.scale = state.scaleVelocity.scale;
}
return isDone;
}
function createOptions(initOptions) {
const state = {
options: initOptions || {},
overscrollBounds: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
};
function calculateOverscrollBounds() {
state.overscrollBounds = parseEdges(state.options.overscrollBounds, 30);
}
function setAllOptions(newOptions) {
if (newOptions) {
state.options = { ...newOptions };
} else {
state.options = {};
}
calculateOverscrollBounds();
}
function set(key, value) {
state.options[key] = value;
if (key === "overscrollBounds") {
calculateOverscrollBounds();
}
}
calculateOverscrollBounds();
return {
setAllOptions,
set,
get hasBlockingRects() {
return !!state.options.getBlockingRects;
},
get overscrollBounds() {
return state.overscrollBounds;
},
get minScale() {
return withDefault(state.options.minScale, 0.1);
},
get maxScale() {
return withDefault(state.options.maxScale, 5);
},
get scrollStepAmount() {
return withDefault(state.options.scrollStepAmount, 256);
},
get margin() {
return withDefault(state.options.margin, 20);
},
get momentumDeceleration() {
return withDefault(state.options.momentumDeceleration, 0.96);
},
get springDamping() {
return withDefault(state.options.springDamping, 0.5);
},
get direction() {
return state.options.direction || "both";
},
get rootClientRectMaxStale() {
return withDefault(state.options.rootClientRectMaxStale, 5e3);
},
get blockingRects() {
if (state.options.getBlockingRects) {
const rects = state.options.getBlockingRects();
return rects.map((rect) => {
if (Array.isArray(rect)) {
return {
x: rect[0],
y: rect[1],
width: rect[2],
height: rect[3]
};
}
return rect;
});
}
return [];
}
};
}
function createArtboard(providedRootEl, initPlugins = [], initOptions = {}) {
const options = createOptions(initOptions);
const state = {
animation: null,
artboardSize: null,
interaction: "none",
lastAnimateToTimestamp: 0,
lastLoopTimestamp: 0,
offset: {
x: asValidNumber(initOptions?.initTransform?.x, 0),
y: asValidNumber(initOptions?.initTransform?.y, 0)
},
rootRect: providedRootEl.getBoundingClientRect(),
rootSize: {
width: providedRootEl.offsetWidth,
height: providedRootEl.offsetHeight
},
scale: asValidNumber(initOptions?.initTransform?.scale, 1),
touchDirection: "none",
momentum: null,
momentumStopTimestamp: 0,
scaleVelocity: null
};
const rootEl = providedRootEl;
let plugins = [];
let resizeTimeout = null;
let init = true;
let lastRootRectUpdate = 0;
function handleResize(entries) {
for (const entry of entries) {
if (entry.target === rootEl) {
if (entry.target instanceof HTMLElement) {
updateRootRect(true);
}
const size = entry.contentBoxSize[0];
if (!size) {
return;
}
state.rootSize.width = size.inlineSize;
state.rootSize.height = size.blockSize;
} else {
for (const plugin of plugins) {
if (plugin.onSizeChange) {
plugin.onSizeChange(entry);
}
}
}
}
}
const resizeObserver = new ResizeObserver(function(entries) {
if (init) {
init = false;
handleResize(entries);
return;
}
if (resizeTimeout) {
window.clearTimeout(resizeTimeout);
}
resizeTimeout = window.setTimeout(function() {
handleResize(entries);
}, 300);
});
function setOptions(newOptions) {
options.setAllOptions(newOptions);
}
function setOption(key, value) {
options.set(key, value);
}
function getScale() {
return state.scale;
}
function getFinalScale() {
if (state.animation) {
return state.animation.scale;
} else if (state.scaleVelocity) {
return state.scaleVelocity.scale;
}
return state.scale;
}
function getOffset() {
return { ...state.offset };
}
function getFinalOffset() {
if (state.animation) {
return {
x: state.animation.x,
y: state.animation.y
};
}
return { ...state.offset };
}
function destroy() {
plugins.forEach((plugin) => {
if (plugin.destroy) {
plugin.destroy();
}
});
resizeObserver.disconnect();
}
function animateToBoundary() {
const boundaries = getBoundaries();
const targetX = clamp(state.offset.x, boundaries.xMin, boundaries.xMax);
const targetY = clamp(state.offset.y, boundaries.yMin, boundaries.yMax);
if (Math.abs(targetX - state.offset.x) > 0.1 || Math.abs(targetY - state.offset.y) > 0.1) {
setMomentum(state.offset.x - targetX, state.offset.y - targetY);
setInteraction("momentum");
}
}
function stopMomentum() {
animateToBoundary();
setInteraction();
setTouchDirection();
state.momentumStopTimestamp = performance.now();
state.momentum = null;
state.scaleVelocity = null;
state.animation = null;
}
function loop(currentTime) {
if (!state.lastLoopTimestamp) {
state.lastLoopTimestamp = currentTime;
}
const boundaries = getBoundaries();
if (state.interaction === "momentum") {
const shouldStop = applyMomentum(state, currentTime, boundaries);
if (shouldStop) {
stopMomentum();
}
} else if (state.interaction === "momentumScaling") {
const shouldStop = applyScaleMomentum(
state,
currentTime,
options.minScale,
options.maxScale
);
if (shouldStop) {
stopMomentum();
}
} else if (state.animation) {
const isFinished = applyAnimation(state, currentTime, state.animation);
if (isFinished) {
state.animation = null;
}
}
const ctx = {
rootSize: {
...state.rootSize
},
artboardSize: state.artboardSize ? {
...state.artboardSize
} : null,
offset: {
...state.offset
},
scale: state.scale,
boundaries,
currentTime
};
for (let i = 0; i < plugins.length; i++) {
if (plugins[i].loop) {
plugins[i].loop(ctx);
}
}
state.lastLoopTimestamp = currentTime;
return ctx;
}
function getCenterX(targetScale) {
if (!state.artboardSize) {
return state.offset.x;
}
const scaleToUse = targetScale || state.scale;
const blockingRects = options.blockingRects;
return calculateCenterPosition(
blockingRects,
rootEl.getBoundingClientRect(),
state.artboardSize.width * scaleToUse
).centerX;
}
function animateTo(key, targetX, targetY, targetScale, animationOptions) {
setInteraction("none");
const direction = options.direction;
const x = direction === "both" || direction === "horizontal" ? targetX : getCenterX(targetScale);
const y = direction === "both" || direction === "vertical" ? targetY : 0;
state.animation = {
key,
x,
y,
scale: targetScale || state.scale,
startX: state.offset.x,
startY: state.offset.y,
startScale: state.scale,
easing: animationOptions?.easing || "easeOutCubic",
duration: animationOptions?.duration || 400,
startTime: 0
};
state.momentum = null;
state.scaleVelocity = null;
}
function getArtboardSize() {
if (!state.artboardSize) {
return null;
}
return {
width: state.artboardSize.width,
height: state.artboardSize.height
};
}
function getRootSize() {
return {
width: state.rootSize.width,
height: state.rootSize.height
};
}
function animateOrJumpBy(providedX, providedY, options2) {
const x = typeof providedX === "number" ? providedX : 0;
const y = typeof providedY === "number" ? providedY : 0;
const diff = performance.now() - state.lastAnimateToTimestamp;
if (diff < 300) {
setOffset(
(state.animation?.x || state.offset.x) + x,
(state.animation?.y || state.offset.y) + y,
true
);
state.animation = null;
} else {
const limited = limitOffset(
state.offset.x + x,
state.offset.y + y,
getBoundaries()
);
animateTo("animateOrJumpBy", limited.x, limited.y, state.scale, options2);
}
state.lastAnimateToTimestamp = performance.now();
}
function animateOrJumpTo(providedX, providedY, options2) {
const x = typeof providedX === "number" ? providedX : state.offset.x;
const y = typeof providedY === "number" ? providedY : state.offset.y;
const diff = performance.now() - state.lastAnimateToTimestamp;
if (diff < 300) {
setOffset(x, y, true);
state.animation = null;
} else {
animateTo("animateOrJumpTo", x, y, state.scale, options2);
}
state.lastAnimateToTimestamp = performance.now();
}
function scrollPageUp(o) {
animateOrJumpBy(0, state.rootSize.height, o);
}
function scrollPageDown(o) {
animateOrJumpBy(0, -state.rootSize.height, o);
}
function scrollPageLeft(o) {
animateOrJumpBy(state.rootSize.width, null, o);
}
function scrollPageRight(o) {
animateOrJumpBy(-state.rootSize.width, null, o);
}
function scrollUp(amount, o) {
animateOrJumpBy(0, amount || options.scrollStepAmount, o);
}
function scrollDown(amount, o) {
animateOrJumpBy(0, amount || -options.scrollStepAmount, o);
}
function scrollLeft(amount, o) {
animateOrJumpBy(amount || options.scrollStepAmount, null, o);
}
function scrollRight(amount, o) {
animateOrJumpBy(amount || -options.scrollStepAmount, null, o);
}
function scrollToTop(o) {
animateOrJumpTo(null, options.margin, o);
}
function scrollToEnd(o) {
if (!state.artboardSize) {
return;
}
const v = state.artboardSize.height * state.scale;
const y = -v + state.rootSize.height - options.margin;
animateOrJumpTo(null, y, o);
}
function scaleToFit(scrollOptions) {
if (!state.artboardSize) {
return;
}
scrollIntoView(
{
x: 0,
y: 0,
width: state.artboardSize.width,
height: state.artboardSize.height
},
{
scale: "blocking",
...scrollOptions || {}
}
);
}
function resetZoom(o) {
if (!state.artboardSize) {
return;
}
const animationOptions = {
duration: o?.duration || 300,
easing: o?.easing
};
const viewportCenterY = state.rootSize.height / 2;
const currentCenterOnArtboard = (-state.offset.y + viewportCenterY) / state.scale;
if (state.artboardSize.height < state.rootSize.height) {
const newYOffset2 = state.rootSize.height / 2 - state.artboardSize.height / 2;
return animateTo(
"resetZoom",
getCenterX(1),
newYOffset2,
1,
animationOptions
);
}
const newYOffset = Math.min(
Math.max(
-currentCenterOnArtboard + viewportCenterY,
-state.artboardSize.height + state.rootSize.height - options.overscrollBounds.top
),
options.overscrollBounds.top + options.overscrollBounds.bottom
);
animateTo("resetZoom", getCenterX(1), newYOffset, 1, animationOptions);
}
function getBoundaries(providedTargetScale) {
if (!state.artboardSize) {
return {
xMin: Number.NEGATIVE_INFINITY,
xMax: Number.POSITIVE_INFINITY,
yMin: Number.NEGATIVE_INFINITY,
yMax: Number.POSITIVE_INFINITY
};
}
const targetScale = providedTargetScale || state.scale;
const artboardWidth = state.artboardSize.width * targetScale;
const artboardHeight = state.artboardSize.height * targetScale;
const bounds = options.overscrollBounds;
const paddingTop = Math.min(bounds.top, state.rootSize.height / 4);
const paddingBottom = Math.min(bounds.bottom, state.rootSize.height / 4);
const paddingLeft = Math.min(bounds.left, state.rootSize.width / 4);
const paddingRight = Math.min(bounds.right, state.rootSize.width / 4);
const xMin = -artboardWidth + paddingLeft;
const xMax = state.rootSize.width - paddingRight;
const yMin = -artboardHeight + paddingTop;
const yMax = state.rootSize.height - paddingBottom;
return { xMin, xMax, yMin, yMax };
}
function constrainScale(scale) {
return clamp(scale, options.minScale, options.maxScale);
}
function setScale(newScale, immediate) {
if (immediate) {
state.scale = constrainScale(newScale);
return;
}
state.scale = dampenRelative(
newScale,
options.minScale,
options.maxScale,
0.9
);
}
function zoomIn(delta = 10) {
doZoom(state.rootSize.width / 2, state.rootSize.height / 2, delta);
}
function zoomOut(delta = -10) {
doZoom(state.rootSize.width / 2, state.rootSize.height / 2, delta);
}
function setOffset(providedX, providedY, immediate) {
const direction = options.direction;
const setX = direction === "both" || direction === "horizontal";
const setY = direction === "both" || direction === "vertical";
const x = typeof providedX === "number" && setX ? providedX : state.offset.x;
const y = typeof providedY === "number" && setY ? providedY : state.offset.y;
const boundaries = getBoundaries();
if (immediate) {
const limited = limitOffset(x, y, boundaries);
state.offset.x = limited.x;
state.offset.y = limited.y;
return;
}
state.offset.x = dampenRelative(
x,
boundaries.xMin,
boundaries.xMax,
options.springDamping
);
state.offset.y = dampenRelative(
y,
boundaries.yMin,
boundaries.yMax,
options.springDamping
);
}
function cancelAnimation() {
state.animation = null;
state.momentum = null;
}
function updateRootRect(force) {
const now = performance.now();
if (force || now - lastRootRectUpdate > options.rootClientRectMaxStale) {
state.rootRect = rootEl.getBoundingClientRect();
lastRootRectUpdate = now;
}
}
function calculateScaleAroundPoint(pageX, pageY, targetScale, providedOffset, providedScale) {
updateRootRect();
const newScale = constrainScale(targetScale);
const offset = providedOffset || getOffset();
const scale = providedScale || getScale();
const x = pageX - state.rootRect.x;
const y = pageY - state.rootRect.y;
const transformedX = (x - offset.x) / scale;
const transformedY = (y - offset.y) / scale;
const targetX = -transformedX * newScale + x;
const targetY = -transformedY * newScale + y;
const limited = limitOffset(targetX, targetY, getBoundaries(newScale));
return {
x: limited.x,
y: limited.y,
scale: newScale
};
}
function scaleAroundPoint(pageX, pageY, targetScale, animationOptions) {
updateRootRect();
const newScale = constrainScale(targetScale);
const x = pageX - state.rootRect.x;
const y = pageY - state.rootRect.y;
const transformedX = (x - state.offset.x) / state.scale;
const transformedY = (y - state.offset.y) / state.scale;
const targetX = -transformedX * newScale + x;
const targetY = -transformedY * newScale + y;
const limited = limitOffset(targetX, targetY, getBoundaries(newScale));
if (animationOptions) {
animateTo(
"scaleAroundPoint",
limited.x,
limited.y,
newScale,
typeof animationOptions === "object" ? animationOptions : null
);
return;
}
setScale(newScale, true);
setOffset(limited.x, limited.y, true);
}
function doZoom(x, y, delta) {
const scaleFactor = Math.pow(1.5, Math.sign(delta) / 2);
const newScale = state.scale * scaleFactor;
scaleAroundPoint(x, y, newScale);
}
function scrollIntoView(targetRect, animationOptions) {
const targetWidth = targetRect.width;
const targetHeight = targetRect.height;
const targetX = targetRect.x;
const targetY = targetRect.y;
const scaleOption = animationOptions?.scale || "none";
const targetScale = (() => {
if (scaleOption === "full") {
const scaleX = (state.rootSize.width - options.margin * 2) / targetWidth;
const scaleY = (state.rootSize.height - options.margin * 2) / targetHeight;
return Math.min(scaleX, scaleY, options.maxScale);
} else if (scaleOption === "blocking" && options.hasBlockingRects) {
const { availableWidth } = calculateCenterPosition(
options.blockingRects,
rootEl.getBoundingClientRect(),
targetWidth
);
const targetScale2 = (availableWidth - options.margin * 2) / targetWidth;
const scaleY = (state.rootSize.height - options.margin * 2) / targetHeight;
return Math.min(targetScale2, scaleY, options.maxScale);
}
return 1;
})();
const behavior = animationOptions?.behavior || "auto";
const axis = animationOptions?.axis || "both";
const scrollX = axis === "x" || axis === "both";
const scrollY = axis === "y" || axis === "both";
const centeredOffsetX = (() => {
if (scaleOption === "blocking") {
return getCenterX(targetScale);
}
return scrollX ? -(targetX * targetScale) + (state.rootSize.width - targetWidth * targetScale) / 2 : state.offset.x;
})();
const centeredOffsetY = scrollY ? -(targetY * targetScale) + (state.rootSize.height - targetHeight * targetScale) / 2 : state.offset.y;
if (behavior === "smooth" || behavior === "auto" && !state.animation) {
animateTo(
"scrollIntoView",
centeredOffsetX,
centeredOffsetY,
targetScale,
{
duration: animationOptions?.duration || 300,
easing: animationOptions?.easing || "easeInOutExpo"
}
);
return;
}
cancelAnimation();
setOffset(centeredOffsetX, centeredOffsetY, true);
setScale(targetScale, true);
}
function setArtboardSize(width, height) {
if (!state.artboardSize) {
state.artboardSize = {
width: 0,
height: 0
};
}
state.artboardSize.width = width;
state.artboardSize.height = height;
}
function getRootElement() {
return rootEl;
}
function getInteraction() {
return state.interaction;
}
function setInteraction(v) {
const targetInteraction = v || "none";
if (state.interaction === targetInteraction) {
return;
}
if ((state.interaction === "momentum" || state.interaction === "momentumScaling") && targetInteraction !== "momentum" && targetInteraction !== "momentumScaling") {
state.momentumStopTimestamp = performance.now();
}
state.interaction = targetInteraction;
}
function getMomentum() {
if (state.momentum) {
return {
...state.momentum
};
}
return null;
}
function setMomentum(momentumX, momentumY, deceleration) {
if (momentumX !== void 0 && momentumX !== null && momentumY !== void 0) {
const direction = options.direction;
const x = direction === "both" || direction === "horizontal" ? momentumX : 0;
const y = direction === "both" || direction === "vertical" ? momentumY : 0;
state.momentum = {
x,
y,
deceleration: deceleration || options.momentumDeceleration
};
} else {
state.momentum = null;
}
}
function getTouchDirection() {
return state.touchDirection;
}
function setTouchDirection(v) {
state.touchDirection = v || "none";
}
function startMomentum(velocity) {
const totalVelocity = velocity ? Math.abs(velocity.x) + Math.abs(velocity.y) : null;
if (!velocity || !totalVelocity) {
state.momentum = null;
setInteraction("none");
animateToBoundary();
return;
}
const x = state.touchDirection === "horizontal" || state.touchDirection === "both" ? velocity.x : 0;
const y = state.touchDirection === "vertical" || state.touchDirection === "both" ? velocity.y : 0;
setMomentum(x, y);
setInteraction("momentum");
}
function setDirectionOffset(x, y) {
if (state.touchDirection === "both" || state.touchDirection === "none") {
setOffset(x, y);
} else if (state.touchDirection === "vertical") {
setOffset(state.offset.x, y);
} else if (state.touchDirection === "horizontal") {
setOffset(x, state.offset.y);
}
}
function setScaleTarget(newX, newY, scale) {
const direction = options.direction;
const x = direction === "both" || direction === "horizontal" ? newX : getCenterX();
const y = direction === "both" || direction === "vertical" ? newY : 0;
if (state.scaleVelocity) {
state.scaleVelocity.scale = scale;
state.scaleVelocity.x = x;
state.scaleVelocity.y = y;
} else {
state.scaleVelocity = {
x,
y,
scale
};
}
}
function getScaleTarget() {
if (state.scaleVelocity) {
return {
...state.scaleVelocity
};
}
return null;
}
function scrollElementIntoView(targetEl, options2) {
const targetBoundingRect = targetEl.getBoundingClientRect();
const targetWidth = targetEl.offsetWidth;
const targetHeight = targetEl.offsetHeight;
const scale = getScale();
const targetX = (targetBoundingRect.x - state.offset.x - state.rootRect.x) / scale;
const targetY = (targetBoundingRect.y - state.offset.y - state.rootRect.y) / scale;
scrollIntoView(
{
width: targetWidth,
height: targetHeight,
x: targetX,
y: targetY
},
options2
);
}
function observeSize(el) {
resizeObserver.observe(el);
}
function unobserveSize(el) {
resizeObserver.unobserve(el);
}
function getAnimation() {
if (state.animation) {
return { ...state.animation };
}
return null;
}
function wasMomentumScrolling() {
return performance.now() - state.momentumStopTimestamp < 200;
}
const artboard = {
wasMomentumScrolling,
addPlugin,
animateOrJumpBy,
animateOrJumpTo,
animateTo,
animateToBoundary,
startMomentum,
cancelAnimation,
destroy,
getArtboardSize,
getCenterX,
getFinalOffset,
getFinalScale,
getInteraction,
getOffset,
getRootElement,
getRootSize,
getScale,
getTouchDirection,
getMomentum,
getAnimation,
loop,
observeSize,
options,
removePlugin,
resetZoom,
scaleAroundPoint,
scaleToFit,
scrollDown,
scrollElementIntoView,
scrollIntoView,
scrollLeft,
scrollPageDown,
scrollPageLeft,
scrollPageRight,
scrollPageUp,
scrollRight,
scrollToEnd,
scrollToTop,
scrollUp,
setArtboardSize,
setDirectionOffset,
setInteraction,
setOffset,
setOption,
setOptions,
setScale,
setTouchDirection,
setMomentum,
unobserveSize,
zoomIn,
zoomOut,
getBoundaries,
setScaleTarget,
calculateScaleAroundPoint,
getScaleTarget
};
function addPlugin(plugin) {
const pluginInstance = plugin.init(artboard, plugin.options);
plugins.push(pluginInstance);
return pluginInstance;
}
function removePlugin(plugin) {
if (plugin.destroy) {
plugin.destroy();
}
plugins = plugins.filter((v) => v !== plugin);
}
resizeObserver.observe(rootEl);
if (initPlugins?.length) {
initPlugins.forEach((plugin) => addPlugin(plugin));
}
return artboard;
}
function inlineStyleOverrider(element) {
const overridenStyles = /* @__PURE__ */ new Map();
const overridenProperties = /* @__PURE__ */ new Map();
const prevValues = /* @__PURE__ */ new Map();
function set(property, value) {
const prev = prevValues.get(property);
if (prev === value) {
return;
}
if (prev === void 0) {
overridenStyles.set(property, element.style[property]);
}
element.style[property] = typeof value === "number" ? value + "px" : value;
prevValues.set(property, value);
}
function setProperty(property, value) {
const prev = prevValues.get(property);
if (prev === value) {
return;
}
if (prev === void 0) {
overridenProperties.set(
property,
element.style.getPropertyValue(property)
);
}
const propertyValue = typeof value === "number" ? value + "px" : value;
element.style.setProperty(property, propertyValue);
prevValues.set(property, value);
}
function setTransform(x, y, scale, scaleY) {
let transform = `translate3d(${x}px, ${y}px, 0px)`;
if (scale) {
if (scaleY) {
transform += ` scale(${scale}, ${scaleY})`;
} else {
transform += ` scale(${scale})`;
}
}
set("transform", transform);
}
function setMultiple(styles) {
Object.entries(styles).forEach(([property, value]) => {
if (value !== void 0) {
set(property, value);
}
});
}
function restore() {
try {
overridenStyles.entries().forEach(([property, value]) => {
element.style[property] = value;
});
overridenProperties.entries().forEach(([property, value]) => {
element.style.setProperty(property, value);
});
} catch {
}
}
return {
set,
setTransform,
setProperty,
setMultiple,
restore
};
}
function pluginOptions(providedOptions) {
let options = providedOptions;
const computedCache = /* @__PURE__ */ new Map();
function get(key, defaultValue) {
const v = options ? options[key] : void 0;
if (v === void 0 && defaultValue !== void 0) {
return defaultValue;
}
if (typeof v === "number" && Number.isNaN(v)) {
return defaultValue;
}
return v;
}
function getRequired(key) {
const value = get(key);
if (value === void 0 || value === null) {
throw new Error(`Missing required plugin option "${String(key)}".`);
}
return value;
}
function getElement(key, fallbackSelector, parent) {
const value = get(key);
if (value instanceof HTMLElement) {
return value;
} else if (typeof value === "string") {
const possibleElement = parent.querySelector(value);
if (possibleElement instanceof HTMLElement) {
return possibleElement;
}
} else {
const possibleElement = parent.querySelector(fallbackSelector);
if (possibleElement instanceof HTMLElement) {
return possibleElement;
}
}
throw new Error(`Failed to locate element for "${String(key)} option."`);
}
function should(key, defaultValue) {
const v = options ? options[key] : void 0;
if (v === void 0 && defaultValue !== void 0) {
return !!defaultValue;
}
return !!v;
}
function recompute() {
computedCache.forEach((cacheEntry, callback) => {
cacheEntry.value = callback(options);
});
}
function set(key, value) {
if (!options) {
options = {};
}
options[key] = value;
recompute();
}
function setAll(newOptions) {
options = newOptions;
recompute();
}
function computed(callback) {
if (computedCache.has(callback)) {
return computedCache.get(callback);
}
const initialValue = callback(options);
const cacheEntry = { value: initialValue };
computedCache.set(callback, cacheEntry);
return cacheEntry;
}
return {
get,
getRequired,
getElement,
should,
set,
setAll,
computed
};
}
function defineArtboardPlugin(init) {
return function(providedOptions) {
const options = pluginOptions(providedOptions);
return {
options,
init
};
};
}
const clickZoom = defineArtboardPlugin(function(artboard, options) {
const rootEl = artboard.getRootElement();
let mouseStartCoords = null;
function shouldZoom() {
return !artboard.wasMomentumScrolling();
}
function onPointerDown(e) {
if (e.pointerType === "touch") {
destroy();
return;
}
mouseStartCoords = getEventCoords(e);
}
function onPointerUp(e) {
if (!mouseStartCoords) {
return;
}
const distance = getDistance(mouseStartCoords, getEventCoords(e));
if (distance > 10) {
return;
}
if (!shouldZoom()) {
return;
}
doZoom(mouseStartCoords);
}
function getTargetScale() {
const maxScale = artboard.options.maxScale;
const threshold = maxScale * 0.5;
const currentScale = artboard.getFinalScale();
if (currentScale < 1) {
return 1;
}
return currentScale >= threshold ? 1 : maxScale;
}
function doZoom(coords) {
const animation = options.get("animation", {
duration: 500,
easing: "easeInOutExpo"
});
artboard.scaleAroundPoint(coords.x, coords.y, getTargetScale(), animation);
}
function destroy() {
rootEl.removeEventListener("pointerdown", onPointerDown);
rootEl.removeEventListener("pointerup", onPointerUp);
}
rootEl.addEventListener("pointerdown", onPointerDown);
rootEl.addEventListener("pointerup", onPointerUp);
return {
options,
destroy
};
});
const cssProperties = defineArtboardPlugin(function(artboard, options) {
const element = options.get("element") || artboard.getRootElement();
const style = inlineStyleOverrider(element);
function getValue(v, precision) {
if (options.should("unitless")) {
return withPrecision(v, precision).toString();
}
return withPrecision(v, precision);
}
const properties = options.computed(
function(o) {
return Object.fromEntries(o.properties.map((v) => [v, true]));
}
);
function loop(ctx) {
const precision = options.get("precision", 0.5);
if (properties.value["--artboard-offset-x"]) {
const x = getValue(ctx.offset.x, precision);
style.setProperty("--artboard-offset-x", x);
}
if (properties.value["--artboard-offset-y"]) {
const y = getValue(ctx.offset.y, precision);
style.setProperty("--artboard-offset-y", y);
}
if (properties.value["--artboard-size-width"]) {
const width = getValue(ctx.artboardSize?.width || 0, precision);
style.setProperty("--artboard-size-width", width);
}
if (properties.value["--artboard-size-height"]) {
const height = getValue(ctx.artboardSize?.height || 0, precision);
style.setProperty("--artboard-size-height", height);
}
if (properties.value["--artboard-root-width"]) {
const width = getValue(ctx.rootSize.width, precision);
style.setProperty("--artboard-root-width", width);
}
if (properties.value["--artboard-root-height"]) {
const height = getValue(ctx.rootSize.height, precision);
style.setProperty("--artboard-root-height", height);
}
if (properties.value["--artboard-scale"]) {
style.setProperty("--artboard-scale", ctx.scale.toString());
}
}
function destroy() {
if (options.should("restoreProperties")) {
style.restore();
}
}
return {
options,
destroy,
loop
};
});
const dom = defineArtboardPlugin(function(artboard, options) {
const artboardElement = options.getRequired("element");
const styleOverrider = inlineStyleOverrider(artboardElement);
function loop(ctx) {
if (!ctx.artboardSize) {
return;
}
const precision = options.get("precision", 0.5);
const x = withPrecision(ctx.offset.x, precision);
const y = withPrecision(ctx.offset.y, precision);
if (options.should("applyScalePrecision")) {
const scaleX = adjustScaleForPrecision(
ctx.artboardSize.width,
ctx.scale,
precision
);
const scaleY = adjustScaleForPrecision(
ctx.artboardSize.height,
ctx.scale,
precision
);
styleOverrider.setTransform(x, y, scaleX, scaleY);
} else {
styleOverrider.setTransform(x, y, ctx.scale);
}
}
function onSizeChange(entry) {
if (entry.target === artboardElement) {
const size = entry.contentBoxSize[0];
if (!size) {
return;
}
if (entry.target instanceof HTMLImageElement) {
artboard.setArtboardSize(
entry.target.naturalWidth,
entry.target.naturalHeight
);
styleOverrider.set("width", entry.target.naturalWidth);
styleOverrider.set("height", entry.target.naturalHeight);
return;
}
artboard.setArtboardSize(size.inlineSize, size.blockSize);
}
}
function applyInitStyles() {
styleOverrider.setMultiple({
position: "absolute",
top: "0px",
left: "0px",
transformOrigin: "0 0"
});
}
function onImageLoaded() {
if (artboardElement instanceof HTMLImageElement) {
artboard.setArtboardSize(
artboardElement.naturalWidth,
artboardElement.naturalHeight
);
if (options.should("setInitTransformFromRect")) {
setInitTransformFromRect();
}
styleOverrider.set("width", artboardElement.naturalWidth);
styleOverrider.set("height", artboardElement.naturalHeight);
styleOverrider.set("maxWidth", "none");
applyInitStyles();
}
}
function setInitTransformFromRect() {
const artboardRect = artboardElement.getBoundingClientRect();
const rootRect = artboard.getRootElement().getBoundingClientRect();
const x = artboardRect.x - rootRect.x;
const y = artboardRect.y - rootRect.y;
const artboardWidth = artboardElement instanceof HTMLImageElement ? artboardElement.naturalWidth : artboardElement.offsetWidth;
const scale = Math.round(artboardRect.width / artboardWidth * 1e3) / 1e3;
artboard.setScale(scale, true);
artboard.setOffset(x, y, true);
}
if (artboardElement instanceof HTMLImageElement) {
if (artboardElement.complete) {
onImageLoaded();
} else {
artboardElement.addEventListener("load", onImageLoaded);
}
} else {
artboard.setArtboardSize(
artboardElement.offsetWidth,
artboardElement.offsetHeight
);
if (options.should("setInitTransformFromRect")) {
setInitTransformFromRect();
}
applyInitStyles();
artboard.observeSize(artboardElement);
}
function destroy() {
artboard.unobserveSize(artboardElement);
artboardElement.removeEventListener("load", onImageLoaded);
if (options.should("restoreStyles")) {
styleOverrider.restore();
}
}
return {
options,
loop,
destroy,
onSizeChange
};
});
const doubleTapZoom = defineArtboardPlugin(function(artboard, options) {
const rootEl = artboard.getRootElement();
let startTime = 0;
let startOffset = null;
function onTouchStart(e) {
if (e.touches.length !== 1) {
return;
}
const duration = e.timeStamp - startTime;
if (duration < 300 && startOffset) {
const distance = getDistance(startOffset, artboard.getOffset());
if (distance < 10) {
doZoom(getEventCoords(e));
return;
}
}
startTime = e.timeStamp;
startOffset = artboard.getOffset();
}
function doZoom(coords) {
const currentScale = artboard.getFinalScale();
const targetScale = currentScale >= 3 ? 1 : 6;
const animation = options.get("animation", {
duration: 500,
easing: "easeInOutExpo"
});
artboard.scaleAroundPoint(coords.x, coords.y, targetScale, animation);
}
function destroy() {
rootEl.removeEventListener("touchstart", onTouchStart);
}
rootEl.addEventListener("touchstart", onTouchStart);
return {
options,
destroy
};
});
const DEFAULT_KEYMAP = {
ArrowDown: ["scrollDown"],
ArrowUp: ["scrollUp"],
ArrowLeft: ["scrollLeft"],
ArrowRight: ["scrollRight"],
Home: ["scrollToTop"],
End: ["scrollToEnd"],
PageUp: ["scrollPageUp"],
PageDown: ["scrollPageDown"],
Digit0: ["resetZoom", true],
Digit1: ["scaleToFit", true]
};
const keyboard = defineArtboardPlugin(function(artboard, options) {
document.addEventListener("keydown", onKeyDown);
function destroy() {
document.removeEventListener("keydown", onKeyDown);
}
function isPressingModifier(e) {
const modifier = options.get("modifier", "ctrlmeta");
if (modifier === "ctrl") {
return e.ctrlKey;
} else if (modifier === "alt") {
return e.altKey;
} else if (modifier === "meta") {
return e.metaKey;
} else if (modifier === "ctrlmeta") {
return e.metaKey || e.ctrlKey;
}
throw new Error("Invalid modifier key.");
}
function onKeyDown(e) {
const keymap = options.get("keymap", DEFAULT_KEYMAP);
if (!artboard) {
return;
}
const mapping = keymap[e.code];
if (!mapping) {
return;
}
const method = mapping[0];
const needsModifier = !!mapping[1];
if (needsModifier && !isPressingModifier(e)) {
return;
}
e.preventDefault();
artboard[method]();
}
return {
options,
destroy
};
});
function createQueue() {
const queue = [];
const timeQueue = [];
function reset() {
queue.splice(0);
timeQueue.splice(0);
}
function add(position) {
queue.push(position);
timeQueue.push(performance.now());
}
return {
reset,
add,
queue,
timeQueue
};
}
function createDirectionQueue(options) {
const queue = createQueue();
function getDirection(force) {
if (queue.queue.length < 2) {
return void 0;
}
const timeDiff = queue.timeQueue[queue.timeQueue.length - 1] - queue.timeQueue[0];
if (timeDiff > 50 || queue.queue.length > 5 || force) {
let totalDeltaX = 0;
let totalDeltaY = 0;
for (let i = 1; i < queue.queue.length; i++) {
totalDeltaX += queue.queue[i].x - queue.queue[i - 1].x;
totalDeltaY += queue.queue[i].y - queue.queue[i - 1].y;
}
const avgDeltaX = totalDeltaX / (queue.queue.length - 1);
const avgDeltaY = totalDeltaY / (queue.queue.length - 1);
const absAvgDeltaX = Math.abs(avgDeltaX);
const absAvgDeltaY = Math.abs(avgDeltaY);
const angle = Math.atan2(absAvgDeltaY, absAvgDeltaX) * 180 / Math.PI;
const isHorizontal = angle >= -options.threshold && angle <= options.threshold || angle >= 180 - options.threshold || angle <= -180 + options.threshold;
const isVertical = angle >= 90 - options.threshold && angle <= 90 + options.threshold || angle >= -90 - options.threshold && angle <= -90 + options.threshold;
if (isHorizontal && !isVertical) {
return "horizontal";
} else if (!isHorizontal && isVertical) {
return "vertical";
}
return "both";
}
return void 0;
}
function add(v) {
queue.add(v);
}
function reset() {
queue.reset();
}
return {
getDirection,
add,
reset
};
}
function createVelocityQueue(initOptions) {
const queue = createQueue();
let maxTimeWindow = initOptions.maxTimeWindow;
let minVelocity = initOptions.minVelocity;
let maxVelocity = initOptions.maxVelocity;
let multiplicator = initOptions.multiplicator;
function getVelocity() {
const length = queue.timeQueue.length;
if (length < 2) {
return { x: 0, y: 0 };
}
const lastIndex = length - 1;
const lastPos = queue.queue[lastIndex];
const lastTime = queue.timeQueue[lastIndex];
let weightedVelocityX = 0;
let weightedVelocityY = 0;
let totalWeight = 0;
for (let i = lastIndex - 1; i >= 0; i--) {
const timeDiff = lastTime - queue.timeQueue[i];
if (timeDiff > maxTimeWindow) {
break;
}
const pos = queue.queue[i];
const distanceX = lastPos.x - pos.x;
const distanceY = lastPos.y - pos.y;
const timeInSeconds = timeDiff / 1e3;
if (timeInSeconds === 0) {
continue;
}
const weight = 1 - timeDiff / maxTimeWindow;
weightedVelocityX += distanceX / timeInSeconds * weight;
weightedVelocityY += distanceY / timeInSeconds * weight;
totalWeight += weight;
}
if (totalWeight === 0) {
return { x: 0, y: 0 };
}
const averageVelocity = {
x: limit(weightedVelocityX / totalWeight * multiplicator),
y: limit(weightedVelocityY / totalWeight * multiplicator)
};
return {
x: Math.abs(averageVelocity.x) >= minVelocity ? averageVelocity.x : 0,
y: Math.abs(averageVelocity.y) >= minVelocity ? averageVelocity.y : 0
};
}
function limit(v) {
return Math.min(Math.max(v, -maxVelocity), maxVelocity);
}
function add(