UNPKG

piling.js

Version:

A WebGL-based Library for Visual Piling/Stacking

395 lines (329 loc) 10.9 kB
import { assign, identity, l2PointDist, nextAnimationFrame, pipe, throttleAndDebounce, wait, withConstructor, withStaticProperty, } from '@flekschas/utils'; import * as PIXI from 'pixi.js'; import { ifNotNull } from './utils'; import { DEFAULT_DARK_MODE, DEFAULT_LASSO_FILL_OPACITY, DEFAULT_LASSO_SHOW_START_INDICATOR, DEFAULT_LASSO_START_INDICATOR_OPACITY, DEFAULT_LASSO_STROKE_OPACITY, DEFAULT_LASSO_STROKE_SIZE, LASSO_MIN_DELAY, LASSO_MIN_DIST, LASSO_SHOW_START_INDICATOR_TIME, LASSO_HIDE_START_INDICATOR_TIME, } from './defaults'; const lassoStyleEl = document.createElement('style'); document.head.appendChild(lassoStyleEl); const lassoStylesheets = lassoStyleEl.sheet; const addRule = (rule) => { const currentNumRules = lassoStylesheets.length; lassoStylesheets.insertRule(rule, currentNumRules); return currentNumRules; }; const removeRule = (index) => { lassoStylesheets.deleteRule(index); }; const inAnimation = `${LASSO_SHOW_START_INDICATOR_TIME}ms ease scaleInFadeOut 0s 1 normal backwards`; const createInAnimationRule = (currentOpacity, currentScale) => ` @keyframes scaleInFadeOut { 0% { opacity: ${currentOpacity}; transform: translate(-50%,-50%) scale(${currentScale}); } 10% { opacity: 1; transform: translate(-50%,-50%) scale(1); } 100% { opacity: 0; transform: translate(-50%,-50%) scale(0.9); } } `; let inAnimationRuleIndex = null; const outAnimation = `${LASSO_HIDE_START_INDICATOR_TIME}ms ease fadeScaleOut 0s 1 normal backwards`; const createOutAnimationRule = (currentOpacity, currentScale) => ` @keyframes fadeScaleOut { 0% { opacity: ${currentOpacity}; transform: translate(-50%,-50%) scale(${currentScale}); } 100% { opacity: 0; transform: translate(-50%,-50%) scale(0); } } `; let outAnimationRuleIndex = null; const createLasso = ({ fillColor: initialFillColor = null, fillOpacity: initialFillOpacity = DEFAULT_LASSO_FILL_OPACITY, isShowStartIndicator: initialIsShowStartIndicator = DEFAULT_LASSO_SHOW_START_INDICATOR, isDarkMode: initialIsDarkMode = DEFAULT_DARK_MODE, onDraw: initialOnDraw = identity, onStart: initialOnStart = identity, startIndicatorOpacity: initialStartIndicatorOpacity = DEFAULT_LASSO_START_INDICATOR_OPACITY, strokeColor: initialStrokeColor = null, strokeOpacity: initialStrokeOpacity = DEFAULT_LASSO_STROKE_OPACITY, strokeSize: initialStrokeSize = DEFAULT_LASSO_STROKE_SIZE, } = {}) => { let fillColor = initialFillColor; let fillOpacity = initialFillOpacity; let isShowStartIndicator = initialIsShowStartIndicator; let isDarkMode = initialIsDarkMode; let startIndicatorOpacity = initialStartIndicatorOpacity; let strokeColor = initialStrokeColor; let strokeOpacity = initialStrokeOpacity; let strokeSize = initialStrokeSize; let onDraw = initialOnDraw; let onStart = initialOnStart; const lineContainer = new PIXI.Container(); const fillContainer = new PIXI.Container(); const lineGfx = new PIXI.Graphics(); const fillGfx = new PIXI.Graphics(); lineContainer.addChild(lineGfx); fillContainer.addChild(fillGfx); const getLassoFillColor = () => fillColor || isDarkMode ? 0xffffff : 0x000000; const getLassoStrokeColor = () => fillColor || isDarkMode ? 0xffffff : 0x000000; const getBackgroundColor = () => isDarkMode ? `rgba(255, 255, 255, ${startIndicatorOpacity})` : `rgba(0, 0, 0, ${startIndicatorOpacity})`; const startIndicator = document.createElement('div'); startIndicator.id = 'lasso-start-indicator'; startIndicator.style.position = 'absolute'; startIndicator.style.zIndex = 1; startIndicator.style.width = '4rem'; startIndicator.style.height = '4rem'; startIndicator.style.borderRadius = '4rem'; startIndicator.style.opacity = 0.5; startIndicator.style.transform = 'translate(-50%,-50%) scale(0)'; let isMouseDown = false; let isLasso = false; let lassoPos = []; let lassoPosFlat = []; let lassoPrevMousePos; const mouseUpHandler = () => { isMouseDown = false; }; const indicatorClickHandler = (event) => { const parent = event.target.parentElement; if (!parent) return; const rect = parent.getBoundingClientRect(); showStartIndicator([event.clientX - rect.left, event.clientY - rect.top]); }; const indicatorMouseDownHandler = () => { isMouseDown = true; isLasso = true; clear(); onStart(); }; const indicatorMouseLeaveHandler = () => { hideStartIndicator(); }; window.addEventListener('mouseup', mouseUpHandler); const resetStartIndicatorStyle = () => { startIndicator.style.opacity = 0.5; startIndicator.style.transform = 'translate(-50%,-50%) scale(0)'; }; const getCurrentStartIndicatorAnimationStyle = () => { const computedStyle = getComputedStyle(startIndicator); const opacity = +computedStyle.opacity; // The css rule `transform: translate(-1, -1) scale(0.5);` is represented as // `matrix(0.5, 0, 0, 0.5, -1, -1)` const m = computedStyle.transform.match(/([0-9.-]+)+/g); const scale = m ? +m[0] : 1; return { opacity, scale }; }; const showStartIndicator = async ([x, y]) => { await wait(0); if (isMouseDown) return; let opacity = 0.5; let scale = 0; if (isShowStartIndicator) { const style = getCurrentStartIndicatorAnimationStyle(); opacity = style.opacity; scale = style.scale; startIndicator.style.opacity = opacity; startIndicator.style.transform = `translate(-50%,-50%) scale(${scale})`; } startIndicator.style.animation = 'none'; // See https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Tips // why we need to wait for two animation frames await nextAnimationFrame(2); startIndicator.style.top = `${y}px`; startIndicator.style.left = `${x}px`; if (inAnimationRuleIndex !== null) removeRule(inAnimationRuleIndex); inAnimationRuleIndex = addRule(createInAnimationRule(opacity, scale)); startIndicator.style.animation = inAnimation; await nextAnimationFrame(); resetStartIndicatorStyle(); }; const hideStartIndicator = async () => { const { opacity, scale } = getCurrentStartIndicatorAnimationStyle(); startIndicator.style.opacity = opacity; startIndicator.style.transform = `translate(-50%,-50%) scale(${scale})`; startIndicator.style.animation = 'none'; await nextAnimationFrame(2); if (outAnimationRuleIndex !== null) removeRule(outAnimationRuleIndex); outAnimationRuleIndex = addRule(createOutAnimationRule(opacity, scale)); startIndicator.style.animation = outAnimation; await nextAnimationFrame(); resetStartIndicatorStyle(); }; const draw = () => { lineGfx.clear(); fillGfx.clear(); if (lassoPos.length) { lineGfx.lineStyle(strokeSize, getLassoStrokeColor(), strokeOpacity); lineGfx.moveTo(...lassoPos[0]); lassoPos.forEach((pos) => { lineGfx.lineTo(...pos); lineGfx.moveTo(...pos); }); fillGfx.beginFill(getLassoFillColor(), fillOpacity); fillGfx.drawPolygon(lassoPosFlat); } onDraw(); }; const extend = (currMousePos) => { if (!lassoPrevMousePos) { if (!isLasso) { isLasso = true; onStart(); } lassoPos = [currMousePos]; lassoPosFlat = [currMousePos[0], currMousePos[1]]; lassoPrevMousePos = currMousePos; } else { const d = l2PointDist( currMousePos[0], currMousePos[1], lassoPrevMousePos[0], lassoPrevMousePos[1] ); if (d > LASSO_MIN_DIST) { lassoPos.push(currMousePos); lassoPosFlat.push(currMousePos[0], currMousePos[1]); lassoPrevMousePos = currMousePos; if (lassoPos.length > 1) { draw(); } } } }; const extendDb = throttleAndDebounce( extend, LASSO_MIN_DELAY, LASSO_MIN_DELAY ); const clear = () => { lassoPos = []; lassoPosFlat = []; lassoPrevMousePos = undefined; draw(); }; const end = () => { isLasso = false; const lassoPoints = [...lassoPos]; extendDb.cancel(); clear(); return lassoPoints; }; const set = ({ fillColor: newFillColor = null, fillOpacity: newFillOpacity = null, onDraw: newOnDraw = null, onStart: newOnStart = null, showStartIndicator: newIsShowStartIndicator = null, startIndicatorOpacity: newStartIndicatorOpacity = null, strokeColor: newStrokeColor = null, strokeOpacity: newStrokeOpacity = null, strokeSize: newStrokeSize = null, darkMode: newIsDarkMode = null, } = {}) => { fillColor = ifNotNull(newFillColor, fillColor); fillOpacity = ifNotNull(newFillOpacity, fillOpacity); onDraw = ifNotNull(newOnDraw, onDraw); onStart = ifNotNull(newOnStart, onStart); isDarkMode = ifNotNull(newIsDarkMode, isDarkMode); isShowStartIndicator = ifNotNull( newIsShowStartIndicator, isShowStartIndicator ); startIndicatorOpacity = ifNotNull( newStartIndicatorOpacity, startIndicatorOpacity ); strokeColor = ifNotNull(newStrokeColor, strokeColor); strokeOpacity = ifNotNull(newStrokeOpacity, strokeOpacity); strokeSize = ifNotNull(newStrokeSize, strokeSize); startIndicator.style.background = getBackgroundColor(); if (isShowStartIndicator) { startIndicator.addEventListener('click', indicatorClickHandler); startIndicator.addEventListener('mousedown', indicatorMouseDownHandler); startIndicator.addEventListener('mouseleave', indicatorMouseLeaveHandler); } else { startIndicator.removeEventListener( 'mousedown', indicatorMouseDownHandler ); startIndicator.removeEventListener( 'mouseleave', indicatorMouseLeaveHandler ); } }; const destroy = () => { window.removeEventListener('mouseup', mouseUpHandler); startIndicator.removeEventListener('click', indicatorClickHandler); startIndicator.removeEventListener('mousedown', indicatorMouseDownHandler); startIndicator.removeEventListener( 'mouseleave', indicatorMouseLeaveHandler ); }; const withPublicMethods = () => (self) => assign(self, { clear, destroy, end, extend, extendDb, set, showStartIndicator, }); set({ fillColor, fillOpacity, isShowStartIndicator, isDarkMode, onDraw, onStart, startIndicatorOpacity, strokeColor, strokeOpacity, strokeSize, }); return pipe( withStaticProperty('startIndicator', startIndicator), withStaticProperty('fillContainer', fillContainer), withStaticProperty('lineContainer', lineContainer), withPublicMethods(), withConstructor(createLasso) )({}); }; export default createLasso;