import { ConfigEntity, Entity, PositionData, PositionSchema, SizeData, SizeSchema, TransformData } from '../../../common'; import { startTween } from '../../utils'; import { Rectangle } from '@gedit/math'; import { Selectable } from '../../able'; import { Disposable, Emitter, PromiseDeferred } from '@gedit/utils'; import { domUtils } from '@gedit/utils/lib/browser'; export interface PlaygroundConfigEntityData { scrollX: number // 滚动x scrollY: number // 滚动y originX: number // 左上角默认开始的原点坐标 originY: number // 左上角默认开始原点坐标 width: number // 编辑区宽,在onResize触发后重制 height: number // 编辑区高,在onResize触发后重制 clientX: number // 如果有拖拽场景需要传入 clientY: number // 如果有拖拽场景需要传入 resolution: number // 分辨率 reverseScroll: boolean // 支持反方向滚动 overflowX: 'hidden' | 'scroll' overflowY: 'hidden' | 'scroll' minZoom: number, // 最大 maxZoom: number, // 最小 zoom: number // 缩放比 scrollLimitX?: number // 水平滚动限制 scrollLimitY?: number // 垂直滚动限制 pageBounds?: { x: number, y: number, width: number, height: number }, // 编辑的画布边框, 用于处理外部对齐问题 loadingHTML: string } export interface PlaygroundConfigRevealOpts { entities?: Entity[] position?: PositionSchema, // 滚动到指定位置, 并居中 bounds?: Rectangle // 滚动的bounds selection?: boolean // 是否回到选择器所在位置,默认true scrollDelta?: PositionSchema, zoom?: number, // 需要缩放的比例 easing?: boolean // 是否开启缓动, 默认开启 easingDuration?: number // 默认 500 ms scrollToCenter?: boolean, // 是否滚动到中心 } /** * 全局画布的配置信息 */ export class PlaygroundConfigEntity extends ConfigEntity { static type = 'PlaygroundConfigEntity'; private _loading = false; private _zoomEnable = true; protected onLoadingChangedEmitter = new Emitter(); readonly onLoadingChanged = this.onLoadingChangedEmitter.event; cursor = 'default'; getDefaultConfig(): PlaygroundConfigEntityData { return { scrollX: 0, scrollY: 0, originX: 0, originY: 0, width: 0, height: 0, minZoom: 0.1, maxZoom: 100, zoom: 1, resolution: 1, clientX: 0, clientY: 0, reverseScroll: true, overflowX: 'scroll', overflowY: 'scroll', loadingHTML: '加载中...', }; } /** * 更新实体配置 * @param props */ updateConfig(props: Partial): void { if (props.zoom !== undefined) { props = { ...props, zoom: this.normalizeZoom(props.zoom) }; } props = { ...this.config, ...props }; if (!props.reverseScroll) { if (props.scrollX! < this.config.originX) { props.scrollX = this.config.originX; } if (props.scrollY! < this.config.originY) { props.scrollY = this.config.originY; } } if (props.scrollLimitX !== undefined && props.scrollX! > props.scrollLimitX) { props.scrollX = props.scrollLimitX; } if (props.scrollLimitY !== undefined && props.scrollY! > props.scrollLimitY) { props.scrollY = props.scrollLimitY; } if (props.overflowX === 'hidden') { props.scrollX = this.config.originX; } if (props.overflowY === 'hidden') { props.scrollY = this.config.originY; } super.updateConfig(props); } /** * 最终缩放比 */ get finalScale(): number { if (!this.zoomEnable) return 1 / this.config.resolution; return this.config.zoom / this.config.resolution; } get scrollData(): { scrollX: number, scrollY: number } { return { scrollX: this.config.scrollX, scrollY: this.config.scrollY, }; } protected normalizeZoom(zoom: number): number { if (!this.zoomEnable) return 1; if (zoom < this.config.minZoom) { zoom = this.config.minZoom; } else if (zoom > this.config.maxZoom) { zoom = this.config.maxZoom; } return zoom; } /** * 修改画布光标 * @param cursor */ updateCursor(cursor: string): void { if (this.cursor !== cursor) { this.cursor = cursor; this.fireChanged(); } } /** * 获取相对画布的位置 * @param event * @param widthScale 是否要计算缩放 */ getPosFromMouseEvent(event: { clientX: number, clientY: number }, withScale = true): PositionSchema { const config = this.config; const scale = withScale ? this.finalScale : 1; return { x: (event.clientX + config.scrollX - config.clientX) / scale, y: (event.clientY + config.scrollY - config.clientY) / scale, }; } /** * 将画布中的位置转成相对window的位置 * @param pos */ toFixedPos(pos: PositionSchema): PositionSchema { const config = this.config; return { x: pos.x - config.scrollX + config.clientX, y: pos.y - config.scrollY + config.clientY }; } /** * 获取可视区域 */ getBoundsVisible(withScale: boolean = true): Rectangle { const config = this.config; const scale = withScale ? this.finalScale : 1; return new Rectangle(config.scrollX / scale, config.scrollY / scale, config.width / scale, config.height / scale); } /** * 判断矩形是否在可视区域 * @param bounds * @param rotation * @param includeAll - 是否包含在里边, 默认false, TODO allVisible判断 暂时不支持旋转后的情况 */ isBoundsVisible(bounds: Rectangle, rotation: number = 0, includeAll: boolean = false): boolean { const boundsVisible = this.getBoundsVisible(); if (includeAll) { return bounds.left >= boundsVisible.left && bounds.right <= boundsVisible.right && bounds.top >= boundsVisible.top && bounds.bottom <= boundsVisible.bottom; } if (rotation === 0) return Rectangle.intersects(bounds, boundsVisible); return Rectangle.intersectsWithRotation(bounds, rotation, boundsVisible, 0); } /** * 按下边顺序执行 * 1. 指定的entity位置或pos位置 * 2. selection位置 * 3. 初始化位置 */ scrollToView(opts: PlaygroundConfigRevealOpts = {}): Promise { const {scrollDelta, position: pos, selection = true, easing = true, easingDuration = 300, entities} = opts; const config = this.config; const scale = opts.zoom ? opts.zoom / this.config.resolution : this.finalScale; let bounds: Rectangle | undefined; if (entities && entities.length > 0) { const entitiesBounds = entities.map(e => { const transform = e.getData(TransformData); if (transform) return transform.bounds; const position = e.getData(PositionData); const size = e.getData(SizeData) || { width: 0, height: 0 }; if (!position) return; return new Rectangle(position.x, position.y, size.width, size.height || 0); }).filter(e => !!e) as Rectangle[]; if (entitiesBounds.length > 0) { bounds = Rectangle.enlarge(entitiesBounds); } } else if (pos) { bounds = new Rectangle(pos.x, pos.y, 0, 0); } else if (opts.bounds) { bounds = opts.bounds; } else if (selection) { bounds = Selectable.getSelectedBounds(this.entityManager); } if (!bounds) { const defaultConfig = this.getDefaultConfig(); bounds = new Rectangle((defaultConfig.scrollX + config.width / 2) / scale, (defaultConfig.scrollY + config.height / 2) / scale, 0, 0); } if (!opts.scrollToCenter) { const boundsVisible = this.getBoundsVisible(); // 判断是否看得见 if (boundsVisible.containsRectangle(bounds)) { return Promise.resolve(); } } // TODO 微调滚动,而不是直接滚动到中心 const toValues = { scrollX: (bounds.x + bounds.width / 2 + (scrollDelta ? scrollDelta.x : 0)) * scale - config.width / 2, scrollY: (bounds.y + bounds.height / 2 + (scrollDelta ? scrollDelta.y : 0)) * scale - config.height / 2, zoom: opts.zoom, }; return this.scroll(toValues, easing, easingDuration); } /** * 这只画布边框,元素编辑的时候回吸附画布边框 * @param bounds */ setPageBounds(bounds: Rectangle): void { this.updateConfig({ pageBounds: { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height } }); } getPageBounds(): Rectangle | undefined { const pageBounds = this.config.pageBounds; if (pageBounds) { return new Rectangle(pageBounds.x, pageBounds.y, pageBounds.width, pageBounds.height); } } /** * 滚动到画布中央 * @param zoomToFit 是否缩放并适配外围大小 * @param fitPadding 适配外围的留白 * @param easing 是否缓动 */ scrollPageBoundsToCenter(zoomToFit: boolean = true, fitPadding = 16, easing = true): Promise { const pageBounds = this.getPageBounds(); if (pageBounds) { let zoom: number | undefined; const fitPaddingDouble = fitPadding * 2; if (zoomToFit) { const fixedScale = SizeSchema.fixSize({ width: pageBounds.width, height: pageBounds.height, }, { width: fitPaddingDouble > this.config.width ? fitPaddingDouble : this.config.width - fitPaddingDouble, height: fitPaddingDouble > this.config.height ? fitPaddingDouble : this.config.height - fitPaddingDouble }); zoom = fixedScale * this.config.resolution; } return this.scrollToView({ bounds: pageBounds, zoom, scrollToCenter: true, selection: false, easing, }); } else { return this.scrollToView({ easing }); } } private cancelScrollTeeen?: Disposable; /** * 滚动 * @param scroll * @param easing - 是否开启缓动, 默认开启 * @param easingDuration - 滚动持续时间, 默认300ms */ scroll(scroll: Partial<{ scrollX: number, scrollY: number, zoom: number }>, easing: boolean = true, easingDuration = 300): Promise { const deferred = new PromiseDeferred(); if (this.cancelScrollTeeen) this.cancelScrollTeeen.dispose(); if (easing) { const fromValues = { scrollX: this.config.scrollX, scrollY: this.config.scrollY, zoom: this.config.zoom, }; this.cancelScrollTeeen = startTween({ from: fromValues, to: { ...fromValues, ...scroll, }, onUpdate: v => { this.updateConfig(v); }, onComplete: () => { this.cancelScrollTeeen = undefined; deferred.resolve(); }, onDispose: () => { deferred.resolve(); }, duration: easingDuration, }); } else { this.updateConfig(scroll); deferred.resolve(); } return deferred.promise; } /** * 让layer的node节点不随着画布滚动条滚动 * @param layerNode */ fixLayerPosition(layerNode: HTMLElement): void { domUtils.setStyle(layerNode, { left: this.config.scrollX, top: this.config.scrollY, }); } get loading(): boolean { return this._loading; } set loading(loading: boolean) { if (this.loading !== loading) { this._loading = loading; this.fireChanged(); this.onLoadingChangedEmitter.fire(loading); } } get zoomEnable(): boolean { return this._zoomEnable; } /** * 开启缩放 * @param zoomEnable */ set zoomEnable(zoomEnable: boolean) { if (this._zoomEnable !== zoomEnable) { this._zoomEnable = zoomEnable; this.fireChanged(); } } /** * 放大 */ zoomin(easing?: boolean, easingDuration?: number): void { const unit = this.config.zoom / 10; const newZoom = Math.ceil((this.config.zoom + unit) * 10) / 10; this.updateZoom(newZoom, easing, easingDuration); } /** * 缩小 */ zoomout(easing?: boolean, easingDuration?: number): void { const unit = this.config.zoom / 10; const newZoom = Math.floor((this.config.zoom - unit) * 10) / 10; this.updateZoom(newZoom, easing, easingDuration); } updateZoom(newZoom: number, easing: boolean = true, easingDuration = 200): void { newZoom = this.normalizeZoom(newZoom); const center = this.getBoundsVisible().center; const oldScale = this.finalScale; const newScale = !this.zoomEnable ? oldScale : newZoom / this.config.resolution; if (newScale !== oldScale) { const delta = { x: center.x * newScale - center.x * oldScale, y: center.y * newScale - center.y * oldScale }; this.scroll({ scrollX: this.config.scrollX + delta.x, scrollY: this.config.scrollY + delta.y, zoom: newZoom, }, easing, easingDuration); } } }