UNPKG

konva

Version:

HTML5 2d canvas library.

705 lines (704 loc) 25.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Stage = exports.stages = void 0; const Util_1 = require("./Util"); const Factory_1 = require("./Factory"); const Container_1 = require("./Container"); const Global_1 = require("./Global"); const Canvas_1 = require("./Canvas"); const DragAndDrop_1 = require("./DragAndDrop"); const Global_2 = require("./Global"); const PointerEvents = require("./PointerEvents"); const STAGE = 'Stage', STRING = 'string', PX = 'px', MOUSEOUT = 'mouseout', MOUSELEAVE = 'mouseleave', MOUSEOVER = 'mouseover', MOUSEENTER = 'mouseenter', MOUSEMOVE = 'mousemove', MOUSEDOWN = 'mousedown', MOUSEUP = 'mouseup', POINTERMOVE = 'pointermove', POINTERDOWN = 'pointerdown', POINTERUP = 'pointerup', POINTERCANCEL = 'pointercancel', LOSTPOINTERCAPTURE = 'lostpointercapture', POINTEROUT = 'pointerout', POINTERLEAVE = 'pointerleave', POINTEROVER = 'pointerover', POINTERENTER = 'pointerenter', CONTEXTMENU = 'contextmenu', TOUCHSTART = 'touchstart', TOUCHEND = 'touchend', TOUCHMOVE = 'touchmove', TOUCHCANCEL = 'touchcancel', WHEEL = 'wheel', MAX_LAYERS_NUMBER = 5, EVENTS = [ [MOUSEENTER, '_pointerenter'], [MOUSEDOWN, '_pointerdown'], [MOUSEMOVE, '_pointermove'], [MOUSEUP, '_pointerup'], [MOUSELEAVE, '_pointerleave'], [TOUCHSTART, '_pointerdown'], [TOUCHMOVE, '_pointermove'], [TOUCHEND, '_pointerup'], [TOUCHCANCEL, '_pointercancel'], [MOUSEOVER, '_pointerover'], [WHEEL, '_wheel'], [CONTEXTMENU, '_contextmenu'], [POINTERDOWN, '_pointerdown'], [POINTERMOVE, '_pointermove'], [POINTERUP, '_pointerup'], [POINTERCANCEL, '_pointercancel'], [LOSTPOINTERCAPTURE, '_lostpointercapture'], ]; const EVENTS_MAP = { mouse: { [POINTEROUT]: MOUSEOUT, [POINTERLEAVE]: MOUSELEAVE, [POINTEROVER]: MOUSEOVER, [POINTERENTER]: MOUSEENTER, [POINTERMOVE]: MOUSEMOVE, [POINTERDOWN]: MOUSEDOWN, [POINTERUP]: MOUSEUP, [POINTERCANCEL]: 'mousecancel', pointerclick: 'click', pointerdblclick: 'dblclick', }, touch: { [POINTEROUT]: 'touchout', [POINTERLEAVE]: 'touchleave', [POINTEROVER]: 'touchover', [POINTERENTER]: 'touchenter', [POINTERMOVE]: TOUCHMOVE, [POINTERDOWN]: TOUCHSTART, [POINTERUP]: TOUCHEND, [POINTERCANCEL]: TOUCHCANCEL, pointerclick: 'tap', pointerdblclick: 'dbltap', }, pointer: { [POINTEROUT]: POINTEROUT, [POINTERLEAVE]: POINTERLEAVE, [POINTEROVER]: POINTEROVER, [POINTERENTER]: POINTERENTER, [POINTERMOVE]: POINTERMOVE, [POINTERDOWN]: POINTERDOWN, [POINTERUP]: POINTERUP, [POINTERCANCEL]: POINTERCANCEL, pointerclick: 'pointerclick', pointerdblclick: 'pointerdblclick', }, }; const getEventType = (type) => { if (type.indexOf('pointer') >= 0) { return 'pointer'; } if (type.indexOf('touch') >= 0) { return 'touch'; } return 'mouse'; }; const getEventsMap = (eventType) => { const type = getEventType(eventType); if (type === 'pointer') { return Global_1.Konva.pointerEventsEnabled && EVENTS_MAP.pointer; } if (type === 'touch') { return EVENTS_MAP.touch; } if (type === 'mouse') { return EVENTS_MAP.mouse; } }; function checkNoClip(attrs = {}) { if (attrs.clipFunc || attrs.clipWidth || attrs.clipHeight) { Util_1.Util.warn('Stage does not support clipping. Please use clip for Layers or Groups.'); } return attrs; } const NO_POINTERS_MESSAGE = `Pointer position is missing and not registered by the stage. Looks like it is outside of the stage container. You can set it manually from event: stage.setPointersPositions(event);`; exports.stages = []; class Stage extends Container_1.Container { constructor(config) { super(checkNoClip(config)); this._pointerPositions = []; this._changedPointerPositions = []; this._buildDOM(); this._bindContentEvents(); exports.stages.push(this); this.on('widthChange.konva heightChange.konva', this._resizeDOM); this.on('visibleChange.konva', this._checkVisibility); this.on('clipWidthChange.konva clipHeightChange.konva clipFuncChange.konva', () => { checkNoClip(this.attrs); }); this._checkVisibility(); } _validateAdd(child) { const isLayer = child.getType() === 'Layer'; const isFastLayer = child.getType() === 'FastLayer'; const valid = isLayer || isFastLayer; if (!valid) { Util_1.Util.throw('You may only add layers to the stage.'); } } _checkVisibility() { if (!this.content) { return; } const style = this.visible() ? '' : 'none'; this.content.style.display = style; } setContainer(container) { if (typeof container === STRING) { if (container.charAt(0) === '.') { const className = container.slice(1); container = document.getElementsByClassName(className)[0]; } else { var id; if (container.charAt(0) !== '#') { id = container; } else { id = container.slice(1); } container = document.getElementById(id); } if (!container) { throw 'Can not find container in document with id ' + id; } } this._setAttr('container', container); if (this.content) { if (this.content.parentElement) { this.content.parentElement.removeChild(this.content); } container.appendChild(this.content); } return this; } shouldDrawHit() { return true; } clear() { const layers = this.children, len = layers.length; for (let n = 0; n < len; n++) { layers[n].clear(); } return this; } clone(obj) { if (!obj) { obj = {}; } obj.container = typeof document !== 'undefined' && document.createElement('div'); return Container_1.Container.prototype.clone.call(this, obj); } destroy() { super.destroy(); const content = this.content; if (content && Util_1.Util._isInDocument(content)) { this.container().removeChild(content); } const index = exports.stages.indexOf(this); if (index > -1) { exports.stages.splice(index, 1); } Util_1.Util.releaseCanvas(this.bufferCanvas._canvas, this.bufferHitCanvas._canvas); return this; } getPointerPosition() { const pos = this._pointerPositions[0] || this._changedPointerPositions[0]; if (!pos) { Util_1.Util.warn(NO_POINTERS_MESSAGE); return null; } return { x: pos.x, y: pos.y, }; } _getPointerById(id) { return this._pointerPositions.find((p) => p.id === id); } getPointersPositions() { return this._pointerPositions; } getStage() { return this; } getContent() { return this.content; } _toKonvaCanvas(config) { config = config || {}; config.x = config.x || 0; config.y = config.y || 0; config.width = config.width || this.width(); config.height = config.height || this.height(); const canvas = new Canvas_1.SceneCanvas({ width: config.width, height: config.height, pixelRatio: config.pixelRatio || 1, }); const _context = canvas.getContext()._context; const layers = this.children; if (config.x || config.y) { _context.translate(-1 * config.x, -1 * config.y); } layers.forEach(function (layer) { if (!layer.isVisible()) { return; } const layerCanvas = layer._toKonvaCanvas(config); _context.drawImage(layerCanvas._canvas, config.x, config.y, layerCanvas.getWidth() / layerCanvas.getPixelRatio(), layerCanvas.getHeight() / layerCanvas.getPixelRatio()); }); return canvas; } getIntersection(pos) { if (!pos) { return null; } const layers = this.children, len = layers.length, end = len - 1; for (let n = end; n >= 0; n--) { const shape = layers[n].getIntersection(pos); if (shape) { return shape; } } return null; } _resizeDOM() { const width = this.width(); const height = this.height(); if (this.content) { this.content.style.width = width + PX; this.content.style.height = height + PX; } this.bufferCanvas.setSize(width, height); this.bufferHitCanvas.setSize(width, height); this.children.forEach((layer) => { layer.setSize({ width, height }); layer.draw(); }); } add(layer, ...rest) { if (arguments.length > 1) { for (let i = 0; i < arguments.length; i++) { this.add(arguments[i]); } return this; } super.add(layer); const length = this.children.length; if (length > MAX_LAYERS_NUMBER) { Util_1.Util.warn('The stage has ' + length + ' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'); } layer.setSize({ width: this.width(), height: this.height() }); layer.draw(); if (Global_1.Konva.isBrowser) { this.content.appendChild(layer.canvas._canvas); } return this; } getParent() { return null; } getLayer() { return null; } hasPointerCapture(pointerId) { return PointerEvents.hasPointerCapture(pointerId, this); } setPointerCapture(pointerId) { PointerEvents.setPointerCapture(pointerId, this); } releaseCapture(pointerId) { PointerEvents.releaseCapture(pointerId, this); } getLayers() { return this.children; } _bindContentEvents() { if (!Global_1.Konva.isBrowser) { return; } EVENTS.forEach(([event, methodName]) => { this.content.addEventListener(event, (evt) => { this[methodName](evt); }, { passive: false }); }); } _pointerenter(evt) { this.setPointersPositions(evt); const events = getEventsMap(evt.type); if (events) { this._fire(events.pointerenter, { evt: evt, target: this, currentTarget: this, }); } } _pointerover(evt) { this.setPointersPositions(evt); const events = getEventsMap(evt.type); if (events) { this._fire(events.pointerover, { evt: evt, target: this, currentTarget: this, }); } } _getTargetShape(evenType) { let shape = this[evenType + 'targetShape']; if (shape && !shape.getStage()) { shape = null; } return shape; } _pointerleave(evt) { const events = getEventsMap(evt.type); const eventType = getEventType(evt.type); if (!events) { return; } this.setPointersPositions(evt); const targetShape = this._getTargetShape(eventType); const eventsEnabled = !(Global_1.Konva.isDragging() || Global_1.Konva.isTransforming()) || Global_1.Konva.hitOnDragEnabled; if (targetShape && eventsEnabled) { targetShape._fireAndBubble(events.pointerout, { evt: evt }); targetShape._fireAndBubble(events.pointerleave, { evt: evt }); this._fire(events.pointerleave, { evt: evt, target: this, currentTarget: this, }); this[eventType + 'targetShape'] = null; } else if (eventsEnabled) { this._fire(events.pointerleave, { evt: evt, target: this, currentTarget: this, }); this._fire(events.pointerout, { evt: evt, target: this, currentTarget: this, }); } this.pointerPos = null; this._pointerPositions = []; } _pointerdown(evt) { const events = getEventsMap(evt.type); const eventType = getEventType(evt.type); if (!events) { return; } this.setPointersPositions(evt); let triggeredOnShape = false; this._changedPointerPositions.forEach((pos) => { const shape = this.getIntersection(pos); DragAndDrop_1.DD.justDragged = false; Global_1.Konva['_' + eventType + 'ListenClick'] = true; if (!shape || !shape.isListening()) { this[eventType + 'ClickStartShape'] = undefined; return; } if (Global_1.Konva.capturePointerEventsEnabled) { shape.setPointerCapture(pos.id); } this[eventType + 'ClickStartShape'] = shape; shape._fireAndBubble(events.pointerdown, { evt: evt, pointerId: pos.id, }); triggeredOnShape = true; const isTouch = evt.type.indexOf('touch') >= 0; if (shape.preventDefault() && evt.cancelable && isTouch) { evt.preventDefault(); } }); if (!triggeredOnShape) { this._fire(events.pointerdown, { evt: evt, target: this, currentTarget: this, pointerId: this._pointerPositions[0].id, }); } } _pointermove(evt) { const events = getEventsMap(evt.type); const eventType = getEventType(evt.type); if (!events) { return; } if (Global_1.Konva.isDragging() && DragAndDrop_1.DD.node.preventDefault() && evt.cancelable) { evt.preventDefault(); } this.setPointersPositions(evt); const eventsEnabled = !(Global_1.Konva.isDragging() || Global_1.Konva.isTransforming()) || Global_1.Konva.hitOnDragEnabled; if (!eventsEnabled) { return; } const processedShapesIds = {}; let triggeredOnShape = false; const targetShape = this._getTargetShape(eventType); this._changedPointerPositions.forEach((pos) => { const shape = (PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos)); const pointerId = pos.id; const event = { evt: evt, pointerId }; const differentTarget = targetShape !== shape; if (differentTarget && targetShape) { targetShape._fireAndBubble(events.pointerout, { ...event }, shape); targetShape._fireAndBubble(events.pointerleave, { ...event }, shape); } if (shape) { if (processedShapesIds[shape._id]) { return; } processedShapesIds[shape._id] = true; } if (shape && shape.isListening()) { triggeredOnShape = true; if (differentTarget) { shape._fireAndBubble(events.pointerover, { ...event }, targetShape); shape._fireAndBubble(events.pointerenter, { ...event }, targetShape); this[eventType + 'targetShape'] = shape; } shape._fireAndBubble(events.pointermove, { ...event }); } else { if (targetShape) { this._fire(events.pointerover, { evt: evt, target: this, currentTarget: this, pointerId, }); this[eventType + 'targetShape'] = null; } } }); if (!triggeredOnShape) { this._fire(events.pointermove, { evt: evt, target: this, currentTarget: this, pointerId: this._changedPointerPositions[0].id, }); } } _pointerup(evt) { const events = getEventsMap(evt.type); const eventType = getEventType(evt.type); if (!events) { return; } this.setPointersPositions(evt); const clickStartShape = this[eventType + 'ClickStartShape']; const clickEndShape = this[eventType + 'ClickEndShape']; const processedShapesIds = {}; let triggeredOnShape = false; this._changedPointerPositions.forEach((pos) => { const shape = (PointerEvents.getCapturedShape(pos.id) || this.getIntersection(pos)); if (shape) { shape.releaseCapture(pos.id); if (processedShapesIds[shape._id]) { return; } processedShapesIds[shape._id] = true; } const pointerId = pos.id; const event = { evt: evt, pointerId }; let fireDblClick = false; if (Global_1.Konva['_' + eventType + 'InDblClickWindow']) { fireDblClick = true; clearTimeout(this[eventType + 'DblTimeout']); } else if (!DragAndDrop_1.DD.justDragged) { Global_1.Konva['_' + eventType + 'InDblClickWindow'] = true; clearTimeout(this[eventType + 'DblTimeout']); } this[eventType + 'DblTimeout'] = setTimeout(function () { Global_1.Konva['_' + eventType + 'InDblClickWindow'] = false; }, Global_1.Konva.dblClickWindow); if (shape && shape.isListening()) { triggeredOnShape = true; this[eventType + 'ClickEndShape'] = shape; shape._fireAndBubble(events.pointerup, { ...event }); if (Global_1.Konva['_' + eventType + 'ListenClick'] && clickStartShape && clickStartShape === shape) { shape._fireAndBubble(events.pointerclick, { ...event }); if (fireDblClick && clickEndShape && clickEndShape === shape) { shape._fireAndBubble(events.pointerdblclick, { ...event }); } } } else { this[eventType + 'ClickEndShape'] = null; if (Global_1.Konva['_' + eventType + 'ListenClick']) { this._fire(events.pointerclick, { evt: evt, target: this, currentTarget: this, pointerId, }); } if (fireDblClick) { this._fire(events.pointerdblclick, { evt: evt, target: this, currentTarget: this, pointerId, }); } } }); if (!triggeredOnShape) { this._fire(events.pointerup, { evt: evt, target: this, currentTarget: this, pointerId: this._changedPointerPositions[0].id, }); } Global_1.Konva['_' + eventType + 'ListenClick'] = false; if (evt.cancelable && eventType !== 'touch' && eventType !== 'pointer') { evt.preventDefault(); } } _contextmenu(evt) { this.setPointersPositions(evt); const shape = this.getIntersection(this.getPointerPosition()); if (shape && shape.isListening()) { shape._fireAndBubble(CONTEXTMENU, { evt: evt }); } else { this._fire(CONTEXTMENU, { evt: evt, target: this, currentTarget: this, }); } } _wheel(evt) { this.setPointersPositions(evt); const shape = this.getIntersection(this.getPointerPosition()); if (shape && shape.isListening()) { shape._fireAndBubble(WHEEL, { evt: evt }); } else { this._fire(WHEEL, { evt: evt, target: this, currentTarget: this, }); } } _pointercancel(evt) { this.setPointersPositions(evt); const shape = PointerEvents.getCapturedShape(evt.pointerId) || this.getIntersection(this.getPointerPosition()); if (shape) { shape._fireAndBubble(POINTERUP, PointerEvents.createEvent(evt)); } PointerEvents.releaseCapture(evt.pointerId); } _lostpointercapture(evt) { PointerEvents.releaseCapture(evt.pointerId); } setPointersPositions(evt) { const contentPosition = this._getContentPosition(); let x = null, y = null; evt = evt ? evt : window.event; if (evt.touches !== undefined) { this._pointerPositions = []; this._changedPointerPositions = []; Array.prototype.forEach.call(evt.touches, (touch) => { this._pointerPositions.push({ id: touch.identifier, x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, }); }); Array.prototype.forEach.call(evt.changedTouches || evt.touches, (touch) => { this._changedPointerPositions.push({ id: touch.identifier, x: (touch.clientX - contentPosition.left) / contentPosition.scaleX, y: (touch.clientY - contentPosition.top) / contentPosition.scaleY, }); }); } else { x = (evt.clientX - contentPosition.left) / contentPosition.scaleX; y = (evt.clientY - contentPosition.top) / contentPosition.scaleY; this.pointerPos = { x: x, y: y, }; this._pointerPositions = [{ x, y, id: Util_1.Util._getFirstPointerId(evt) }]; this._changedPointerPositions = [ { x, y, id: Util_1.Util._getFirstPointerId(evt) }, ]; } } _setPointerPosition(evt) { Util_1.Util.warn('Method _setPointerPosition is deprecated. Use "stage.setPointersPositions(event)" instead.'); this.setPointersPositions(evt); } _getContentPosition() { if (!this.content || !this.content.getBoundingClientRect) { return { top: 0, left: 0, scaleX: 1, scaleY: 1, }; } const rect = this.content.getBoundingClientRect(); return { top: rect.top, left: rect.left, scaleX: rect.width / this.content.clientWidth || 1, scaleY: rect.height / this.content.clientHeight || 1, }; } _buildDOM() { this.bufferCanvas = new Canvas_1.SceneCanvas({ width: this.width(), height: this.height(), }); this.bufferHitCanvas = new Canvas_1.HitCanvas({ pixelRatio: 1, width: this.width(), height: this.height(), }); if (!Global_1.Konva.isBrowser) { return; } const container = this.container(); if (!container) { throw 'Stage has no container. A container is required.'; } container.innerHTML = ''; this.content = document.createElement('div'); this.content.style.position = 'relative'; this.content.style.userSelect = 'none'; this.content.className = 'konvajs-content'; this.content.setAttribute('role', 'presentation'); container.appendChild(this.content); this._resizeDOM(); } cache() { Util_1.Util.warn('Cache function is not allowed for stage. You may use cache only for layers, groups and shapes.'); return this; } clearCache() { return this; } batchDraw() { this.getChildren().forEach(function (layer) { layer.batchDraw(); }); return this; } } exports.Stage = Stage; Stage.prototype.nodeType = STAGE; (0, Global_2._registerNode)(Stage); Factory_1.Factory.addGetterSetter(Stage, 'container'); if (Global_1.Konva.isBrowser) { document.addEventListener('visibilitychange', () => { exports.stages.forEach((stage) => { stage.batchDraw(); }); }); }