Merge "Introduce components for new image diff UI"

This commit is contained in:
Ben Rohlfs
2021-02-24 17:52:41 +00:00
committed by Gerrit Code Review
9 changed files with 884 additions and 0 deletions

View File

@@ -247,6 +247,38 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
[[DefinitelyTyped]]
DefinitelyTyped
* @types/resize-observer-browser
[[DefinitelyTyped_license]]
----
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
----
[[Polymer-2014]]
Polymer-2014

View File

@@ -3204,6 +3204,38 @@ This software is provided "as is", without any warranty.
----
[[DefinitelyTyped]]
DefinitelyTyped
* @types/resize-observer-browser
[[DefinitelyTyped_license]]
----
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
----
[[Polymer-2014]]
Polymer-2014

View File

@@ -0,0 +1,304 @@
/**
* @license
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
query,
} from 'lit-element';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
import {Dimensions, fitToFrame, Point, Rect} from './util';
/**
* Displays a scaled-down version of an image with a draggable frame for
* choosing a portion of the image to be magnified by other components.
*
* Slotted content can be arbitrary elements, but should be limited to images or
* stacks of image-like elements (e.g. for overlays) with limited interactivity,
* to prevent confusion, as the component only captures a limited set of events.
* Slotted content is scaled to fit the bounds of the component, with
* letterboxing if aspect ratios differ. For slotted content smaller than the
* component, it will cap the scale at 1x and also apply letterboxing.
*/
@customElement('gr-overview-image')
export class GrOverviewImage extends LitElement {
@property({type: Object})
frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
@internalProperty() protected contentStyle: StyleInfo = {};
@internalProperty() protected contentTransformStyle: StyleInfo = {};
@internalProperty() protected frameStyle: StyleInfo = {};
@internalProperty() protected overlayStyle: StyleInfo = {};
@internalProperty() protected dragging = false;
@query('.content-box') protected contentBox!: HTMLDivElement;
@query('.content') protected content!: HTMLDivElement;
@query('.content-transform') protected contentTransform!: HTMLDivElement;
@query('.frame') protected frame!: HTMLDivElement;
private contentBounds: Dimensions = {width: 0, height: 0};
private imageBounds: Dimensions = {width: 0, height: 0};
private scale = 1;
// When grabbing the frame to drag it around, this stores the offset of the
// cursor from the center of the frame at the start of the drag.
private grabOffset: Point = {x: 0, y: 0};
private readonly resizeObserver = new ResizeObserver(
(entries: ResizeObserverEntry[]) => {
for (const entry of entries) {
if (entry.target === this.contentBox) {
this.contentBounds = {
width: entry.contentRect.width,
height: entry.contentRect.height,
};
}
if (entry.target === this.contentTransform) {
this.imageBounds = {
width: entry.contentRect.width,
height: entry.contentRect.height,
};
}
this.updateScale();
}
}
);
static styles = css`
:host {
--overview-image-background-color: #000;
--overview-image-frame-color: #f00;
display: flex;
}
* {
box-sizing: border-box;
}
::slotted(*) {
display: block;
}
.content-box {
border: 1px solid var(--overview-image-background-color);
background-color: var(--overview-iamge-background-color);
width: 100%;
position: relative;
}
.content {
position: absolute;
cursor: pointer;
}
.content-transform {
position: absolute;
transform-origin: top left;
will-change: transform;
}
.frame {
border: 1px solid var(--overview-image-frame-color);
position: absolute;
will-change: transform;
}
.overlay {
position: absolute;
z-index: 10000;
cursor: grabbing;
}
`;
render() {
return html`
<div class="content-box">
<div
class="content"
style="${styleMap({
...this.contentStyle,
})}"
@mousemove="${this.maybeDragFrame}"
@mousedown=${this.clickOverview}
@mouseup="${this.releaseFrame}"
>
<div
class="content-transform"
style="${styleMap(this.contentTransformStyle)}"
>
<slot></slot>
</div>
<div
class="frame"
style="${styleMap({
...this.frameStyle,
cursor: this.dragging ? 'grabbing' : 'grab',
})}"
@mousedown="${this.grabFrame}"
></div>
</div>
<div
class="overlay"
style="${styleMap({
...this.overlayStyle,
display: this.dragging ? 'block' : 'none',
})}"
@mousemove="${this.overlayMouseMove}"
@mouseleave="${this.releaseFrame}"
@mouseup="${this.releaseFrame}"
></div>
</div>
`;
}
firstUpdated() {
this.resizeObserver.observe(this.contentBox);
this.resizeObserver.observe(this.contentTransform);
}
updated(changedProperties: PropertyValues) {
if (changedProperties.has('frameRect')) {
this.updateFrameStyle();
}
}
clickOverview(event: MouseEvent) {
event.preventDefault();
this.updateOverlaySize();
this.dragging = true;
const rect = this.content.getBoundingClientRect();
this.notifyNewCenter({
x: (event.clientX - rect.left) / this.scale,
y: (event.clientY - rect.top) / this.scale,
});
}
grabFrame(event: MouseEvent) {
event.preventDefault();
// Do not bubble up into clickOverview().
event.stopPropagation();
this.updateOverlaySize();
this.dragging = true;
const rect = this.frame.getBoundingClientRect();
const frameCenterX = rect.x + rect.width / 2;
const frameCenterY = rect.y + rect.height / 2;
this.grabOffset = {
x: event.clientX - frameCenterX,
y: event.clientY - frameCenterY,
};
}
maybeDragFrame(event: MouseEvent) {
event.preventDefault();
if (!this.dragging) return;
const rect = this.content.getBoundingClientRect();
const center = {
x: (event.clientX - rect.left - this.grabOffset.x) / this.scale,
y: (event.clientY - rect.top - this.grabOffset.y) / this.scale,
};
this.notifyNewCenter(center);
}
releaseFrame(event: MouseEvent) {
event.preventDefault();
this.dragging = false;
this.grabOffset = {x: 0, y: 0};
}
overlayMouseMove(event: MouseEvent) {
event.preventDefault();
this.maybeDragFrame(event);
}
private updateScale() {
const fitted = fitToFrame(this.imageBounds, this.contentBounds);
this.scale = fitted.scale;
this.contentStyle = {
...this.contentStyle,
top: `${fitted.top}px`,
left: `${fitted.left}px`,
width: `${fitted.width}px`,
height: `${fitted.height}px`,
};
this.contentTransformStyle = {
transform: `scale(${this.scale})`,
};
this.updateFrameStyle();
}
private updateFrameStyle() {
const x = this.frameRect.origin.x * this.scale;
const y = this.frameRect.origin.y * this.scale;
const width = this.frameRect.dimensions.width * this.scale;
const height = this.frameRect.dimensions.height * this.scale;
this.frameStyle = {
...this.frameStyle,
transform: `translate(${x}px, ${y}px)`,
width: `${width}px`,
height: `${height}px`,
};
}
private updateOverlaySize() {
const rect = this.contentBox.getBoundingClientRect();
// Create a whole-page overlay to capture mouse events, so that the drag
// interaction continues until the user releases the mouse button. Since
// innerWidth and innerHeight include scrollbars, we subtract 20 pixels each
// to prevent the overlay from extending offscreen under any existing
// scrollbar and causing the scrollbar for the other dimension to show up
// unnecessarily.
const width = window.innerWidth - 20;
const height = window.innerHeight - 20;
this.overlayStyle = {
...this.overlayStyle,
top: `-${rect.top + 1}px`,
left: `-${rect.left + 1}px`,
width: `${width}px`,
height: `${height}px`,
};
}
private notifyNewCenter(center: Point) {
this.dispatchEvent(
new CustomEvent('center-updated', {
detail: {...center},
bubbles: true,
composed: true,
})
);
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-overview-image': GrOverviewImage;
}
}

View File

@@ -0,0 +1,95 @@
/**
* @license
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
css,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
} from 'lit-element';
import {StyleInfo, styleMap} from 'lit-html/directives/style-map';
import {Rect} from './util';
/**
* Displays its slotted content at a given scale, centered over a given point,
* while ensuring the content always fills the container. The content does not
* have to be a single image, it can be arbitrary HTML. To prevent user
* confusion, it should ideally be image-like, i.e. have limited or no
* interactivity, as the component does not prevent events or focus from
* reaching the slotted content.
*/
@customElement('gr-zoomed-image')
export class GrZoomedImage extends LitElement {
@property({type: Number}) scale = 1;
@property({type: Object})
frameRect: Rect = {origin: {x: 0, y: 0}, dimensions: {width: 0, height: 0}};
@internalProperty() protected imageStyles: StyleInfo = {};
static styles = css`
:host {
display: block;
}
::slotted(*) {
display: block;
}
#clip {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
#transform {
position: absolute;
transform-origin: top left;
will-change: transform;
}
`;
render() {
return html`
<div id="clip">
<div id="transform" style="${styleMap(this.imageStyles)}">
<slot></slot>
</div>
</div>
`;
}
updated(changedProperties: PropertyValues) {
if (changedProperties.has('scale') || changedProperties.has('frameRect')) {
this.updateImageStyles();
}
}
private updateImageStyles() {
const {x, y} = this.frameRect.origin;
this.imageStyles = {
'image-rendering': this.scale >= 1 ? 'pixelated' : 'auto',
transform: `translate(${-x}px, ${-y}px) scale(${this.scale})`,
};
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-zoomed-image': GrZoomedImage;
}
}

View File

@@ -0,0 +1,236 @@
/**
* @license
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Point {
x: number;
y: number;
}
export interface Dimensions {
width: number;
height: number;
}
export interface Rect {
origin: Point;
dimensions: Dimensions;
}
export interface FittedContent {
top: number;
left: number;
width: number;
height: number;
scale: number;
}
function clamp(value: number, min: number, max: number) {
return Math.max(min, Math.min(value, max));
}
/**
* Fits content of the given dimensions into the given frame, maintaining the
* aspect ratio of the content and applying letterboxing / pillarboxing as
* needed.
*/
export function fitToFrame(
content: Dimensions,
frame: Dimensions
): FittedContent {
const contentAspectRatio = content.width / content.height;
const frameAspectRatio = frame.width / frame.height;
// If the content is wider than the frame, it will be letterboxed, otherwise
// it will be pillarboxed. When letterboxed, content and frame width will
// match exactly, when pillarboxed, content and frame height will match
// exactly.
const isLetterboxed = contentAspectRatio > frameAspectRatio;
let width: number;
let height: number;
if (isLetterboxed) {
width = Math.min(frame.width, content.width);
height = content.height * (width / content.width);
} else {
height = Math.min(frame.height, content.height);
width = content.width * (height / content.height);
}
const top = (frame.height - height) / 2;
const left = (frame.width - width) / 2;
const scale = width / content.width;
return {top, left, width, height, scale};
}
function ensureInBounds(part: Rect, bounds: Dimensions): Rect {
const x =
part.dimensions.width <= bounds.width
? clamp(part.origin.x, 0, bounds.width - part.dimensions.width)
: (bounds.width - part.dimensions.width) / 2;
const y =
part.dimensions.height <= bounds.height
? clamp(part.origin.y, 0, bounds.height - part.dimensions.height)
: (bounds.height - part.dimensions.height) / 2;
return {origin: {x, y}, dimensions: part.dimensions};
}
/**
* Maintains a given frame inside given bounds, adjusting requested positions
* for the frame as needed. This supports the non-destructive application of a
* scaling factor, so that e.g. the magnification of an image can be changed
* easily while keeping the frame centered over the same spot. Changing bounds
* or frame size also keeps the frame position when possible.
*/
export class FrameConstrainer {
private center: Point = {x: 0, y: 0};
private frameSize: Dimensions = {width: 0, height: 0};
private bounds: Dimensions = {width: 0, height: 0};
private scale = 1;
private unscaledFrame: Rect = {
origin: {x: 0, y: 0},
dimensions: {width: 0, height: 0},
};
private scaledFrame: Rect = {
origin: {x: 0, y: 0},
dimensions: {width: 0, height: 0},
};
getCenter(): Point {
return {...this.center};
}
/**
* Returns the frame at its original size, positioned within the given bounds
* at the given scale; its origin will be in scaled bounds coordinates.
*
* Ex: for given bounds 100x50 and frame size 30x20, centered over (50, 25),
* all at 1x scale, when setting scale to 2, this will return a frame of size
* 30x20, centered over (100, 50), within bounds 200x100.
*
* Useful for positioning a viewport of fixed size over a magnified image.
*/
getUnscaledFrame(): Rect {
return {
origin: {...this.unscaledFrame.origin},
dimensions: {...this.unscaledFrame.dimensions},
};
}
/**
* Returns the scaled down framea scale of 2 will result in frame dimensions
* being halved—position within the given bounds at 1x scale; its origin will
* be in unscaled bounds coordinates.
*
* Ex: for given bounds 100x50 and frame size 30x20, centered over (50, 25),
* all at 1x scale, when setting scale to 2, this will return a frame of size
* 15x10, centered over (50, 25), within bounds 100x50.
*
* Useful for highlighting the magnified portion of an image as determined by
* getUnscaledFrame() in an overview image of fixed size.
*/
getScaledFrame(): Rect {
return {
origin: {...this.scaledFrame.origin},
dimensions: {...this.scaledFrame.dimensions},
};
}
/**
* Requests the frame to be centered over the given point, in unscaled bounds
* coordinates. This will keep the frame within the given bounds, also when
* requesting a center point fully outside the given bounds.
*/
requestCenter(center: Point) {
this.center = {...center};
this.ensureFrameInBounds();
}
/**
* Sets the frame size, while keeping the frame within the given bounds, and
* maintaining the current center if possible.
*/
setFrameSize(frameSize: Dimensions) {
if (frameSize.width <= 0 || frameSize.height <= 0) return;
this.frameSize = {...frameSize};
this.ensureFrameInBounds();
}
/**
* Sets the bounds, while keeping the frame within them, and maintaining the
* current center if possible.
*/
setBounds(bounds: Dimensions) {
if (bounds.width <= 0 || bounds.height <= 0) return;
this.bounds = {...bounds};
this.ensureFrameInBounds();
}
/**
* Sets the applied scale, while keeping the frame within the given bounds,
* and maintaining the current center if possible (both relevant moving from
* a larger scale to a smaller scale).
*/
setScale(scale: number) {
if (!scale || scale <= 0) return;
this.scale = scale;
this.ensureFrameInBounds();
}
private ensureFrameInBounds() {
const scaledCenter = {
x: this.center.x * this.scale,
y: this.center.y * this.scale,
};
const scaledBounds = {
width: this.bounds.width * this.scale,
height: this.bounds.height * this.scale,
};
const scaledFrameSize = {
width: this.frameSize.width / this.scale,
height: this.frameSize.height / this.scale,
};
const requestedUnscaledFrame = {
origin: {
x: scaledCenter.x - this.frameSize.width / 2,
y: scaledCenter.y - this.frameSize.height / 2,
},
dimensions: this.frameSize,
};
const requestedScaledFrame = {
origin: {
x: this.center.x - scaledFrameSize.width / 2,
y: this.center.y - scaledFrameSize.height / 2,
},
dimensions: scaledFrameSize,
};
this.unscaledFrame = ensureInBounds(requestedUnscaledFrame, scaledBounds);
this.scaledFrame = ensureInBounds(requestedScaledFrame, this.bounds);
this.center = {
x: this.scaledFrame.origin.x + this.scaledFrame.dimensions.width / 2,
y: this.scaledFrame.origin.y + this.scaledFrame.dimensions.height / 2,
};
}
}

View File

@@ -0,0 +1,171 @@
/**
* @license
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import '../../../test/common-test-setup-karma.js';
import {FrameConstrainer} from './util.js';
suite('FrameConstrainer tests', () => {
let constrainer;
setup(() => {
constrainer = new FrameConstrainer();
constrainer.setBounds({width: 100, height: 100});
constrainer.setFrameSize({width: 50, height: 50});
constrainer.requestCenter({x: 50, y: 50});
});
suite('changing center', () => {
test('moves frame to requested position', () => {
constrainer.requestCenter({x: 30, y: 30});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 5, y: 5}, dimensions: {width: 50, height: 50}});
});
test('keeps frame in bounds for top left corner', () => {
constrainer.requestCenter({x: 5, y: 5});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 0, y: 0}, dimensions: {width: 50, height: 50}});
});
test('keeps frame in bounds for bottom right corner', () => {
constrainer.requestCenter({x: 95, y: 95});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
});
test('handles out-of-bounds center left', () => {
constrainer.requestCenter({x: -5, y: 50});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 0, y: 25}, dimensions: {width: 50, height: 50}});
});
test('handles out-of-bounds center right', () => {
constrainer.requestCenter({x: 105, y: 50});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 50, y: 25}, dimensions: {width: 50, height: 50}});
});
test('handles out-of-bounds center top', () => {
constrainer.requestCenter({x: 50, y: -5});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 25, y: 0}, dimensions: {width: 50, height: 50}});
});
test('handles out-of-bounds center bottom', () => {
constrainer.requestCenter({x: 50, y: 105});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 25, y: 50}, dimensions: {width: 50, height: 50}});
});
});
suite('changing frame size', () => {
test('maintains center when decreased', () => {
constrainer.setFrameSize({width: 10, height: 10});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 45, y: 45}, dimensions: {width: 10, height: 10}});
});
test('maintains center when increased', () => {
constrainer.setFrameSize({width: 80, height: 80});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 10, y: 10}, dimensions: {width: 80, height: 80}});
});
test('updates center to remain in bounds when increased', () => {
constrainer.setFrameSize({width: 10, height: 10});
constrainer.requestCenter({x: 95, y: 95});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
constrainer.setFrameSize({width: 20, height: 20});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 80, y: 80}, dimensions: {width: 20, height: 20}});
});
});
suite('changing scale', () => {
suite('for unscaled frame', () => {
test('adjusts origin to maintain center when zooming in', () => {
constrainer.setScale(2);
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 75, y: 75}, dimensions: {width: 50, height: 50}});
});
test('adjusts origin to maintain center when zooming out', () => {
constrainer.setFrameSize({width: 20, height: 20});
constrainer.setScale(0.5);
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 15, y: 15}, dimensions: {width: 20, height: 20}});
});
test('keeps frame in bounds when zooming out', () => {
constrainer.setScale(5);
constrainer.requestCenter({x: 100, y: 100});
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 450, y: 450}, dimensions: {width: 50, height: 50}});
constrainer.setScale(1);
assert.deepEqual(
constrainer.getUnscaledFrame(),
{origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
});
});
suite('for scaled frame', () => {
test('decreases frame size and maintains center when zooming in', () => {
constrainer.setScale(2);
assert.deepEqual(
constrainer.getScaledFrame(),
{origin: {x: 37.5, y: 37.5}, dimensions: {width: 25, height: 25}});
});
test('increases frame size and maintains center when zooming out', () => {
constrainer.setFrameSize({width: 20, height: 20});
constrainer.setScale(0.5);
assert.deepEqual(
constrainer.getScaledFrame(),
{origin: {x: 30, y: 30}, dimensions: {width: 40, height: 40}});
});
test('keeps frame in bounds when zooming out', () => {
constrainer.setScale(5);
constrainer.requestCenter({x: 100, y: 100});
assert.deepEqual(
constrainer.getScaledFrame(),
{origin: {x: 90, y: 90}, dimensions: {width: 10, height: 10}});
constrainer.setScale(1);
assert.deepEqual(
constrainer.getScaledFrame(),
{origin: {x: 50, y: 50}, dimensions: {width: 50, height: 50}});
});
});
});
});

View File

@@ -253,6 +253,14 @@ const packages: PackageInfo[] = [
name: "@polymer/polymer",
license: SharedLicenses.Polymer2017
},
{
name: "@types/resize-observer-browser",
license: {
name: 'DefinitelyTyped',
type: LicenseTypes.Mit,
packageLicenseFile: "LICENSE"
}
},
{
name: "@webcomponents/shadycss",
license: SharedLicenses.Polymer2017

View File

@@ -25,6 +25,7 @@
"@polymer/paper-tabs": "^3.1.0",
"@polymer/paper-toggle-button": "^3.0.1",
"@polymer/polymer": "^3.4.1",
"@types/resize-observer-browser": "^0.1.5",
"@webcomponents/shadycss": "^1.9.2",
"@webcomponents/webcomponentsjs": "^1.3.3",
"ba-linkify": "file:../../lib/ba-linkify/src/",

View File

@@ -324,6 +324,11 @@
dependencies:
"@webcomponents/shadycss" "^1.9.1"
"@types/resize-observer-browser@^0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz#36d897708172ac2380cd486da7a3daf1161c1e23"
integrity sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ==
"@webcomponents/shadycss@^1.9.1", "@webcomponents/shadycss@^1.9.2":
version "1.9.4"
resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.4.tgz#4f9d8ea1526bab084c60b53d4854dc39fdb2bb48"