@infinite-canvas-tutorial/webcomponents
Version:
WebComponents UI implementation
253 lines (248 loc) • 9.08 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html, css, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { consume } from '@lit/context';
import { apiContext, appStateContext } from '../context';
import { msg, str } from '@lit/localize';
let PenbarCrop = class PenbarCrop extends LitElement {
constructor() {
super(...arguments);
this.cropRatio = 2;
this.previousLayersCropping = [];
this.originalClipWidth = 0;
this.originalClipHeight = 0;
/** Geometric center of the child node in clip coordinates when entering crop mode, scale around this point when ratio changes */
this.originalCenterX = 0;
this.originalCenterY = 0;
}
get clipNode() {
const { layersCropping } = this.appState;
const [croppingNodeId] = layersCropping;
const node = this.api.getNodeById(croppingNodeId);
return node;
}
get clipChildNode() {
const node = this.clipNode;
if (node) {
return this.api.getNodeByEntity(this.api.getChildren(node)[0]);
}
return null;
}
handleApply() {
this.api.applyCrop();
}
handleCancel() {
this.api.cancelCrop();
}
handleClipAspectChanged(event) {
const value = event.target.value;
if (value === 'original') {
const { x: px, y: py } = this.clipNode;
const { x: cx, y: cy, width: cw, height: ch } = this.clipChildNode;
// Make clip the same size of the original image
this.api.updateNode(this.clipNode, {
x: px + cx,
y: py + cy,
width: cw,
height: ch,
});
this.api.updateNode(this.clipChildNode, {
x: 0,
y: 0,
});
}
else if (value === 'square') {
// 1:1: Use the smaller dimension of the child element as the side length, crop from center
const { x: px, y: py } = this.clipNode;
const { x: cx, y: cy, width: cw, height: ch } = this.clipChildNode;
const side = Math.min(cw, ch);
const offsetX = (cw - side) / 2;
const offsetY = (ch - side) / 2;
this.api.updateNode(this.clipNode, {
x: px + cx + offsetX,
y: py + cy + offsetY,
width: side,
height: side,
});
this.api.updateNode(this.clipChildNode, {
x: -offsetX,
y: -offsetY,
});
}
else if (value.includes(':')) {
const [w, h] = value.split(':').map(Number);
const ratio = w / h;
const { x: px, y: py } = this.clipNode;
const { x: cx, y: cy, width: cw, height: ch } = this.clipChildNode;
// Calculate the largest visible rectangle within the child element according to the ratio, centered
const clipW = Math.min(cw, ratio * ch);
const clipH = clipW / ratio;
const offsetX = (cw - clipW) / 2;
const offsetY = (ch - clipH) / 2;
this.api.updateNode(this.clipNode, {
x: px + cx + offsetX,
y: py + cy + offsetY,
width: clipW,
height: clipH,
});
this.api.updateNode(this.clipChildNode, {
x: -offsetX,
y: -offsetY,
});
}
}
handleCropRatioChanged(e) {
const value = e.target.value;
this.cropRatio = parseFloat(value);
if (this.clipChildNode) {
const child = this.clipChildNode;
const width = this.originalClipWidth * this.cropRatio;
const height = this.originalClipHeight * this.cropRatio;
// Scale around the geometric center recorded when entering crop mode
const x = this.originalCenterX - width / 2;
const y = this.originalCenterY - height / 2;
this.api.updateNode(child, {
x,
y,
width,
height,
});
this.api.record();
}
}
shouldUpdate(changedProperties) {
const newLayersCropping = this.appState.layersCropping;
if (newLayersCropping.length !== this.previousLayersCropping.length) {
this.previousLayersCropping = newLayersCropping;
if (this.clipChildNode) {
const child = this.clipChildNode;
this.cropRatio = child.width / this.clipNode.width;
this.originalClipWidth = this.clipNode.width;
this.originalClipHeight = this.clipNode.height;
// Record the geometric center when entering crop mode (in clip coordinates), scale around this center when ratio changes
this.originalCenterX = child.x + child.width / 2;
this.originalCenterY = child.y + child.height / 2;
}
return true;
}
return super.shouldUpdate(changedProperties);
}
render() {
const { layersCropping } = this.appState;
if (layersCropping.length === 1) {
return html `
<div class="wrapper">
${msg(str `Crop`)}
<sp-divider
size="s"
style="align-self: stretch; height: auto;"
vertical
></sp-divider>
<sp-slider
size="s"
quiet
value=${this.cropRatio}
min=${1}
max=${4}
step=${0.2}
labelVisibility="none"
=${this.handleCropRatioChanged}
></sp-slider>
<sp-divider
size="s"
style="align-self: stretch; height: auto;"
vertical
></sp-divider>
<sp-action-menu size="s" label=${msg(str `Clip Aspect`)} =${this.handleClipAspectChanged}>
<sp-icon-crop slot="icon"></sp-icon-crop>
<sp-menu-item value="original">
${msg(str `Original`)}
</sp-menu-item>
<sp-menu-item value="square">
${msg(str `Square`)}
</sp-menu-item>
<sp-menu-item>
${msg(str `Landscape`)}
<sp-menu slot="submenu" =${this.handleClipAspectChanged}>
<sp-menu-item
value="16:9"
>16:9</sp-menu-item>
<sp-menu-item
value="4:3"
>4:3</sp-menu-item>
<sp-menu-item
value="3:2"
>3:2</sp-menu-item>
</sp-menu>
</sp-menu-item>
<sp-menu-item>
${msg(str `Portrait`)}
<sp-menu slot="submenu" =${this.handleClipAspectChanged}>
<sp-menu-item
value="9:16"
>9:16</sp-menu-item>
<sp-menu-item
value="3:4"
>3:4</sp-menu-item>
<sp-menu-item
value="2:3"
>2:3</sp-menu-item>
</sp-menu>
</sp-menu-item>
</sp-action-menu>
<div class="buttons">
<sp-action-button quiet size="s" =${this.handleCancel}>
<sp-icon-cancel slot="icon"></sp-icon-cancel>
</sp-action-button>
<sp-action-button quiet size="s" =${this.handleApply}>
<sp-icon-checkmark slot="icon"></sp-icon-checkmark>
</sp-action-button>
</div>
</div>
`;
}
}
};
PenbarCrop.styles = css `
.wrapper {
transform: translateX(-50%);
position: absolute;
left: -50%;
bottom: 8px;
display: flex;
gap: var(--spectrum-global-dimension-size-100);
align-items: center;
background: var(--spectrum-gray-100);
border-radius: var(--spectrum-corner-radius-200);
padding: var(--spectrum-global-dimension-size-100);
padding-top: 0;
padding-bottom: 0;
margin: 4px;
filter: drop-shadow(
var(--spectrum-drop-shadow-color) 0px var(--spectrum-drop-shadow-y)
var(--spectrum-drop-shadow-blur)
);
}
.buttons {
display: flex;
}
`;
__decorate([
consume({ context: appStateContext, subscribe: true })
], PenbarCrop.prototype, "appState", void 0);
__decorate([
consume({ context: apiContext, subscribe: true })
], PenbarCrop.prototype, "api", void 0);
__decorate([
state()
], PenbarCrop.prototype, "cropRatio", void 0);
PenbarCrop = __decorate([
customElement('ic-spectrum-penbar-crop')
], PenbarCrop);
export { PenbarCrop };
//# sourceMappingURL=penbar-crop.js.map