@playcanvas/pcui
Version:
User interface component library for the web
1,139 lines (1,136 loc) • 47.1 kB
JavaScript
import { _hsv2rgb, _rgb2hsv } from '../../Math/color-value.mjs';
import { Button } from '../Button/index.mjs';
import { Canvas } from '../Canvas/index.mjs';
import { Element } from '../Element/index.mjs';
import { Label } from '../Label/index.mjs';
import { NumericInput } from '../NumericInput/index.mjs';
import { Overlay } from '../Overlay/index.mjs';
import { Panel } from '../Panel/index.mjs';
import { SelectInput } from '../SelectInput/index.mjs';
import { TextInput } from '../TextInput/index.mjs';
import { math } from '../../../node_modules/playcanvas/build/playcanvas/src/core/math/math.mjs';
import { CurveSet } from '../../../node_modules/playcanvas/build/playcanvas/src/core/math/curve-set.mjs';
import { Curve } from '../../../node_modules/playcanvas/build/playcanvas/src/core/math/curve.mjs';
const CLASS_MULTIPLE_VALUES = 'pcui-multiple-values';
const CURVE_LINEAR = 0;
const CURVE_SPLINE = 4;
const CURVE_STEP = 5;
const REGEX_KEYS = /keys/;
const REGEX_TYPE = /type/;
const CLASS_GRADIENT = 'pcui-gradient';
/**
* Represents a gradient picker.
*/
class GradientPicker extends Element {
/**
* Creates a new GradientPicker.
*
* @param args - The arguments. Extends the Element arguments. Any settable property can also
* be set through the constructor.
*/
constructor(args = {}) {
var _a, _b;
super(args);
this._onKeyDown = (evt) => {
// escape blurs the field
if (evt.key === 'Escape') {
this.blur();
}
// enter opens the gradient picker
if (evt.key !== 'Enter' || !this.enabled || this.readOnly || this.class.contains(CLASS_MULTIPLE_VALUES)) {
return;
}
evt.stopPropagation();
evt.preventDefault();
this._openGradientPicker();
};
this._onFocus = (evt) => {
this.emit('focus');
};
this._onBlur = (evt) => {
this.emit('blur');
};
this._onAnchorsMouseDown = (e) => {
if (this.STATE.hoveredAnchor === -1) {
// user clicked in empty space, create new anchor and select it
const coord = this.calcNormalizedCoord(e.clientX, e.clientY, this.getClientRect(this.UI.anchors.dom));
this.insertAnchor(coord[0], this.evaluateGradient(coord[0]));
this.STATE.anchors = this.calcAnchorTimes();
this.selectAnchor(this.STATE.anchors.indexOf(coord[0]));
}
else if (this.STATE.hoveredAnchor !== this.STATE.selectedAnchor) {
// select the hovered anchor
this.selectAnchor(this.STATE.hoveredAnchor);
}
this.UI.anchors.dom.style.cursor = 'grabbing';
this.UI.anchorAddCrossHair.style.visibility = 'hidden';
// drag the selected anchor
this.dragStart();
this.UI.draggingAnchor = true;
};
this._onAnchorsMouseMove = (e) => {
const coord = this.calcNormalizedCoord(e.clientX, e.clientY, this.getClientRect(this.UI.anchors.dom));
if (this.UI.draggingAnchor) {
this.dragUpdate(math.clamp(coord[0], 0, 1));
this.UI.anchorAddCrossHair.style.visibility = 'hidden';
}
else if (coord[0] >= 0 &&
coord[0] <= 1 &&
coord[1] >= 0 &&
coord[1] <= 1) {
let closest = -1;
let closestDist = 0;
for (let index = 0; index < this.STATE.anchors.length; ++index) {
const dist = Math.abs(this.STATE.anchors[index] - coord[0]);
if (closest === -1 || dist < closestDist) {
closest = index;
closestDist = dist;
}
}
const hoveredAnchor = (closest !== -1 && closestDist < 0.02) ? closest : -1;
if (hoveredAnchor !== this.STATE.hoveredAnchor) {
this.selectHovered(hoveredAnchor);
this.render();
}
if (hoveredAnchor === -1) {
this.UI.anchorAddCrossHair.style.visibility = 'visible';
this.UI.anchors.dom.style.cursor = 'none';
}
else {
this.UI.anchorAddCrossHair.style.visibility = 'hidden';
}
this.UI.showCrosshairPosition.innerText = (Math.round(coord[0] * 100)).toString();
this.UI.anchorAddCrossHair.style.left = `${(2.5 + 320 * coord[0]).toString()}px`;
}
else if (this.STATE.hoveredAnchor !== -1) {
this.UI.anchorAddCrossHair.style.visibility = 'hidden';
this.selectHovered(-1);
this.render();
}
else {
this.UI.anchorAddCrossHair.style.visibility = 'hidden';
}
};
this._onAnchorsMouseUp = (evt) => {
if (this.UI.draggingAnchor) {
this.dragEnd();
this.UI.draggingAnchor = false;
}
this.UI.anchors.dom.style.cursor = 'pointer';
};
this.class.add(CLASS_GRADIENT);
this._canvas = new Canvas({ useDevicePixelRatio: true });
this.dom.appendChild(this._canvas.dom);
this._canvas.parent = this;
this._canvas.on('resize', () => {
this._renderGradient();
});
const canvasElement = this._canvas.dom;
this._checkerboardPattern = this._createCheckerboardPattern(canvasElement.getContext('2d'));
// make sure canvas is the same size as the container element
// 20 times a second
this._resizeInterval = window.setInterval(() => {
this._canvas.resize(this.width, this.height);
}, 1000 / 20);
this.dom.addEventListener('keydown', this._onKeyDown);
this.dom.addEventListener('focus', this._onFocus);
this.dom.addEventListener('blur', this._onBlur);
this.on('click', () => {
if (!this.enabled || this.readOnly || this.class.contains(CLASS_MULTIPLE_VALUES))
return;
this._openGradientPicker();
});
this.renderChanges = (_a = args.renderChanges) !== null && _a !== void 0 ? _a : true;
this.on('change', () => {
if (this.renderChanges) {
this.flash();
}
});
// capture this for the event handler
function genEvtHandler(self, func) {
return function (evt) {
func.apply(self, [evt]);
};
}
this.Helpers = {
rgbaStr: function (color, scale) {
if (!scale) {
scale = 1;
}
let rgba = color.map((element, index) => {
return index < 3 ? Math.round(element * scale) : element;
}).join(',');
for (let i = color.length; i < 4; ++i) {
rgba += `,${i < 3 ? scale : 1}`;
}
return `rgba(${rgba})`;
},
hexStr: function (clr) {
return clr.map((v) => {
return (`00${v.toString(16)}`).slice(-2).toUpperCase();
}).join('');
},
// rgb(a) -> hsva
toHsva: function (rgba) {
const hsva = _rgb2hsv(rgba.map((v) => {
return v * 255;
}));
hsva.push(rgba.length > 3 ? rgba[3] : 1);
return hsva;
},
// hsv(1) -> rgba
toRgba: function (hsva) {
const rgba = _hsv2rgb(hsva).map((v) => {
return v / 255;
});
rgba.push(hsva.length > 3 ? hsva[3] : 1);
return rgba;
},
// calculate the normalized coordinate [x,y] relative to rect
normalizedCoord: function (canvas, x, y) {
const rect = canvas.dom.getBoundingClientRect();
return [
(x - rect.left) / rect.width,
(y - rect.top) / rect.height
];
}
};
this._panel = new Panel();
this._panel.class.add('color-panel');
this.dom.appendChild(this._panel.dom);
this._colorRect = new Canvas({ useDevicePixelRatio: true });
this._colorRect.class.add('color-rect');
this._panel.append(this._colorRect.dom);
this._colorRect.resize(140, 140);
this._colorHandle = document.createElement('div');
this._colorHandle.classList.add('color-handle');
this._panel.append(this._colorHandle);
this._hueRect = new Canvas({ useDevicePixelRatio: true });
this._hueRect.class.add('hue-rect');
this._panel.append(this._hueRect.dom);
this._hueRect.resize(20, 140);
this._hueHandle = document.createElement('div');
this._hueHandle.classList.add('hue-handle');
this._panel.append(this._hueHandle);
this._alphaRect = new Canvas({ useDevicePixelRatio: true });
this._alphaRect.class.add('alpha-rect');
this._panel.append(this._alphaRect.dom);
this._alphaRect.resize(20, 140);
this._alphaHandle = document.createElement('div');
this._alphaHandle.classList.add('alpha-handle');
this._panel.append(this._alphaHandle);
this._fields = document.createElement('div');
this._fields.classList.add('fields');
this._panel.append(this._fields);
this.fieldChangeHandler = genEvtHandler(this, this._onFieldChanged);
this.hexChangeHandler = genEvtHandler(this, this._onHexChanged);
this.downHandler = genEvtHandler(this, this._onMouseDown);
this.moveHandler = genEvtHandler(this, this._onMouseMove);
this.upHandler = genEvtHandler(this, this._onMouseUp);
function numberField(label) {
const field = new NumericInput({
precision: 1,
step: 1,
min: 0,
max: 255
});
field.renderChanges = false;
field.placeholder = label;
field.on('change', this.fieldChangeHandler);
this._fields.appendChild(field.dom);
return field;
}
this._rField = numberField.call(this, 'r');
this._gField = numberField.call(this, 'g');
this._bField = numberField.call(this, 'b');
this._aField = numberField.call(this, 'a');
this._hexField = new TextInput();
this._hexField.renderChanges = false;
this._hexField.placeholder = '#';
this._hexField.on('change', this.hexChangeHandler);
this._fields.appendChild(this._hexField.dom);
// hook up mouse handlers
this._colorRect.dom.addEventListener('mousedown', this.downHandler);
this._hueRect.dom.addEventListener('mousedown', this.downHandler);
this._alphaRect.dom.addEventListener('mousedown', this.downHandler);
this._generateHue(this._hueRect);
this._generateAlpha(this._alphaRect);
this._hsva = [-1, -1, -1, 1];
this._storeHsva = [0, 0, 0, 1];
this._dragMode = 0;
this._changing = false;
this.CONSTANTS = {
bg: '#2c393c',
anchorRadius: 5,
selectedRadius: 7
};
this.UI = {
root: this.dom,
overlay: new Overlay(),
panel: document.createElement('div'),
gradient: new Canvas({ useDevicePixelRatio: true }),
checkerPattern: this.createCheckerPattern(),
anchors: new Canvas({ useDevicePixelRatio: true }),
footer: new Panel(),
typeLabel: new Label({ text: 'Type' }),
typeCombo: new SelectInput({
options: [{ t: '0', v: 'placeholder' }],
type: 'number'
}),
positionLabel: new Label({ text: 'Position' }),
positionEdit: new NumericInput({ min: 0, max: 100, step: 1 }),
copyButton: new Button(),
pasteButton: new Button(),
deleteButton: new Button(),
showSelectedPosition: new NumericInput({ min: 0, max: 100, step: 1, hideSlider: true }),
showCrosshairPosition: document.createElement('div'),
anchorAddCrossHair: document.createElement('div'),
colorPicker: null
};
// current state
this.STATE = {
curves: [], // holds all the gradient curves (either 3 or 4 of them)
keystore: [], // holds the curve during edit
anchors: [], // holds the times of the anchors
hoveredAnchor: -1, // index of the hovered anchor
selectedAnchor: -1, // index of selected anchor
selectedValue: [], // value being dragged
changing: false, // UI is currently changing
draggingAnchor: false,
typeMap: {} // map from curve type dropdown to engine curve enum
};
// initialize overlay
this.UI.root.appendChild(this.UI.overlay.dom);
this.UI.overlay.class.add('picker-gradient');
this.UI.overlay.center = false;
this.UI.overlay.transparent = true;
this.UI.overlay.hidden = true;
this.UI.overlay.clickable = true;
this.UI.overlay.dom.style.position = 'fixed';
this.UI.overlay.on('show', () => {
this.onOpen();
});
this.UI.overlay.on('hide', () => {
this.onClose();
});
// panel
this.UI.panel.classList.add('picker-gradient-panel');
this.UI.overlay.append(this.UI.panel);
// gradient
this.UI.panel.appendChild(this.UI.gradient.dom);
this.UI.gradient.class.add('picker-gradient-gradient');
this.UI.gradient.resize(320, 28);
// anchors
this.UI.panel.appendChild(this.UI.anchors.dom);
this.UI.anchors.class.add('picker-gradient-anchors');
this.UI.anchors.resize(320, 28);
// footer
this.UI.panel.appendChild(this.UI.footer.dom);
this.UI.footer.append(this.UI.typeLabel);
this.UI.footer.class.add('picker-gradient-footer');
this.UI.footer.append(this.UI.typeCombo);
this.UI.typeCombo.value = -1;
this.UI.typeCombo.on('change', (value) => {
this._onTypeChanged(value);
});
// this.UI.footer.append(this.UI.positionLabel);
// this.UI.footer.append(this.UI.positionEdit);
this.UI.positionEdit.style.width = '40px';
this.UI.positionEdit.renderChanges = false;
this.UI.showSelectedPosition.on('change', (value) => {
if (!this.STATE.changing) {
this.moveSelectedAnchor(value / 100);
}
});
this.UI.copyButton.on('click', () => {
this.doCopy();
});
this.UI.copyButton.class.add('copy-curve-button');
this.UI.footer.append(this.UI.copyButton);
// Tooltip.attach({
// target: this.UI.copyButton.dom,
// text: 'Copy',
// align: 'bottom',
// root: this.UI.root
// });
this.UI.pasteButton.on('click', () => {
this.doPaste();
});
this.UI.pasteButton.class.add('paste-curve-button');
this.UI.footer.append(this.UI.pasteButton);
this.UI.deleteButton.on('click', () => {
this.doDelete();
});
this.UI.deleteButton.class.add('delete-curve-button');
this.UI.footer.append(this.UI.deleteButton);
this.UI.panel.appendChild(this._panel.dom);
this.UI.panel.append(this.UI.showSelectedPosition.dom);
this.UI.showSelectedPosition.class.add('show-selected-position');
this.UI.showSelectedPosition._domInput.classList.add('show-selected-position-input');
const crosshairPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
crosshairPath.setAttribute('fill-rule', 'evenodd');
crosshairPath.setAttribute('clip-rule', 'evenodd');
crosshairPath.setAttribute('d', 'M8.5 17C7.35596 17 6.26043 16.7741 5.2134 16.3222C4.16637 15.8703 3.26152 15.2629 2.49882 14.4997C1.73612 13.7366 1.12899 12.8312 0.677391 11.7835C0.225795 10.7359 0 9.6397 0 8.49498C0 7.35026 0.225795 6.25409 0.677391 5.20644C1.12899 4.15879 1.73612 3.25507 2.49882 2.49527C3.26152 1.73548 4.16637 1.12965 5.2134 0.677791C6.26043 0.225928 7.35596 0 8.5 0C9.64404 0 10.7396 0.225928 11.7866 0.677791C12.8336 1.12965 13.7385 1.73548 14.5012 2.49527C15.2639 3.25507 15.871 4.15879 16.3226 5.20644C16.7742 6.25409 17 7.35026 17 8.49498C17 9.6397 16.7742 10.7359 16.3226 11.7835C15.871 12.8312 15.2639 13.7366 14.5012 14.4997C13.7385 15.2629 12.8336 15.8703 11.7866 16.3222C10.7396 16.7741 9.64404 17 8.5 17ZM8.5 2.2593C7.64364 2.2593 6.82576 2.42498 6.04634 2.75635C5.26692 3.08772 4.59622 3.53288 4.03424 4.09185C3.47225 4.65082 3.02568 5.31354 2.69451 6.08004C2.36334 6.84653 2.19776 7.6515 2.19776 8.49498C2.19776 9.6397 2.47875 10.6957 3.04073 11.663C3.60272 12.6303 4.36707 13.3952 5.33383 13.9575C6.30058 14.5198 7.35596 14.8009 8.5 14.8009C9.34298 14.8009 10.1475 14.6353 10.9135 14.3039C11.6796 13.9725 12.3419 13.5257 12.9005 12.9634C13.4592 12.4011 13.9041 11.73 14.2352 10.9501C14.5664 10.1702 14.732 9.35184 14.732 8.49498C14.732 7.6515 14.5664 6.84653 14.2352 6.08004C13.9041 5.31354 13.4592 4.65082 12.9005 4.09185C12.3419 3.53288 11.6796 3.08772 10.9135 2.75635C10.1475 2.42498 9.34298 2.2593 8.5 2.2593ZM9.52361 9.73007V12.9533H7.40614V9.73007H4.11452V7.61134H7.40614V4.31778H9.52361V7.61134H12.745V9.73007H9.52361Z');
crosshairPath.setAttribute('fill', '#FF6600');
const crosshairHolder = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const crosshairBar = document.createElement('div');
crosshairHolder.appendChild(crosshairPath);
crosshairHolder.setAttribute('width', '17');
crosshairHolder.setAttribute('height', '17');
crosshairHolder.setAttribute('viewBox', '0 0 17 17');
this.UI.showCrosshairPosition.classList.add('show-crosshair-position');
crosshairBar.classList.add('crosshair-bar');
this.UI.anchorAddCrossHair.appendChild(crosshairHolder);
this.UI.anchorAddCrossHair.appendChild(crosshairBar);
this.UI.anchorAddCrossHair.appendChild(this.UI.showCrosshairPosition);
this.UI.anchorAddCrossHair.classList.add('anchor-crosshair');
this.UI.anchorAddCrossHair.style.visibility = 'hidden';
this.UI.panel.append(this.UI.anchorAddCrossHair);
// construct the color picker
/* this.on('change', this.colorSelectedAnchor);*/
this.on('changing', function (color) {
this.colorSelectedAnchor(color, true);
});
this._copiedData = null;
this._channels = (_b = args.channels) !== null && _b !== void 0 ? _b : 3;
this._value = this._getDefaultValue();
if (args.value) {
// @ts-ignore
this.value = args.value;
}
}
destroy() {
if (this._destroyed)
return;
this.dom.removeEventListener('keydown', this._onKeyDown);
this.dom.removeEventListener('focus', this._onFocus);
this.dom.removeEventListener('blur', this._onBlur);
window.clearInterval(this._resizeInterval);
super.destroy();
}
_createCheckerboardPattern(context) {
// create checkerboard pattern
const canvas = document.createElement('canvas');
const size = 24;
const halfSize = size / 2;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#';
ctx.fillStyle = '#949a9c';
ctx.fillRect(0, 0, halfSize, halfSize);
ctx.fillRect(halfSize, halfSize, halfSize, halfSize);
ctx.fillStyle = '#657375';
ctx.fillRect(halfSize, 0, halfSize, halfSize);
ctx.fillRect(0, halfSize, halfSize, halfSize);
return context.createPattern(canvas, 'repeat');
}
_getDefaultValue() {
return {
type: 4,
keys: (new Array(this._channels)).fill([0, 0]),
betweenCurves: false
};
}
_openGradientPicker() {
this.callOpenGradientPicker([this.value || this._getDefaultValue()]);
// position picker
const rectPicker = this.getGradientPickerRect();
const rectField = this.dom.getBoundingClientRect();
this.positionGradientPicker(rectField.right - rectPicker.width, rectField.bottom);
// change event from the picker sets the new value
this._evtPickerChanged = this.on('picker:curve:change', this._onPickerChange.bind(this));
// refreshing the value resets the picker
this._evtRefreshPicker = this.on('change', () => this.setGradientPicker([this.value]));
}
_onPickerChange(paths, values) {
const value = this.value || this._getDefaultValue();
// TODO: this is all kinda hacky. We need to clear up
// the events raised by the picker
if (REGEX_KEYS.test(paths[0])) {
// set new value with new keys but same type
this.value = {
type: value.type,
keys: values,
betweenCurves: false
};
}
else if (REGEX_TYPE.test(paths[0])) {
// set new value with new type but same keys
this.value = {
type: values[0],
keys: value.keys,
betweenCurves: false
};
}
}
_renderGradient() {
const canvas = this._canvas.dom;
const context = canvas.getContext('2d');
const width = this._canvas.width;
const height = this._canvas.height;
const ratio = this._canvas.pixelRatio;
context.setTransform(ratio, 0, 0, ratio, 0, 0);
context.fillStyle = this._checkerboardPattern;
context.fillRect(0, 0, width, height);
if (!this.value || !this.value.keys || !this.value.keys.length) {
return;
}
const rgba = [];
const curve = this.channels === 1 ? new CurveSet([this.value.keys]) : new CurveSet(this.value.keys);
curve.type = this.value.type;
const precision = 2;
const gradient = context.createLinearGradient(0, 0, width, 0);
for (let t = precision; t < width; t += precision) {
curve.value(t / width, rgba);
const r = Math.round((rgba[0] || 0) * 255);
const g = Math.round((rgba[1] || 0) * 255);
const b = Math.round((rgba[2] || 0) * 255);
const a = this.channels === 4 ? (rgba[3] || 0) : 1;
gradient.addColorStop(t / width, `rgba(${r}, ${g}, ${b}, ${a})`);
}
context.fillStyle = gradient;
context.fillRect(0, 0, width, height);
}
focus() {
this.dom.focus();
}
blur() {
this.dom.blur();
}
set channels(value) {
if (this._channels === value)
return;
this._channels = Math.max(1, Math.min(value, 4));
// change default value
if (this.value) {
this._renderGradient();
}
}
get channels() {
return this._channels;
}
set value(value) {
// TODO: maybe we should check for equality
// but since this value will almost always be set using
// the picker it's not worth the effort
this._value = value;
this.class.remove(CLASS_MULTIPLE_VALUES);
this._renderGradient();
this.emit('change', value);
this.setValue([value]);
}
get value() {
return this._value;
}
set values(values) {
// we do not support multiple values so just
// add the multiple values class which essentially disables
// the input
this.class.add(CLASS_MULTIPLE_VALUES);
this._renderGradient();
}
_generateHue(canvas) {
const canvasElement = canvas.dom;
const ctx = canvasElement.getContext('2d');
const w = canvas.pixelWidth;
const h = canvas.pixelHeight;
const gradient = ctx.createLinearGradient(0, 0, 0, h);
for (let t = 0; t <= 6; t += 1) {
gradient.addColorStop(t / 6, this.Helpers.rgbaStr(_hsv2rgb([t / 6, 1, 1])));
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
}
_generateAlpha(canvas) {
const canvasElement = canvas.dom;
const ctx = canvasElement.getContext('2d');
const w = canvas.pixelWidth;
const h = canvas.pixelHeight;
const gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, 'rgb(255, 255, 255)');
gradient.addColorStop(1, 'rgb(0, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
}
_generateGradient(canvas, clr) {
const canvasElement = canvas.dom;
const ctx = canvasElement.getContext('2d');
const w = canvas.pixelWidth;
const h = canvas.pixelHeight;
let gradient = ctx.createLinearGradient(0, 0, w, 0);
gradient.addColorStop(0, this.Helpers.rgbaStr([255, 255, 255, 255]));
gradient.addColorStop(1, this.Helpers.rgbaStr([clr[0], clr[1], clr[2], 255]));
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
gradient = ctx.createLinearGradient(0, 0, 0, h);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 255)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
}
_onFieldChanged() {
if (!this._changing) {
const rgba = [
this._rField.value,
this._gField.value,
this._bField.value,
this._aField.value
].map((v) => {
return v / 255;
});
this.hsva = this.Helpers.toHsva(rgba);
this.colorSelectedAnchor(this.color);
}
}
_onHexChanged() {
if (!this._changing) {
const hex = this._hexField.value.trim().toLowerCase();
/* eslint-disable-next-line regexp/no-unused-capturing-group */
if (/^([0-9a-f]{2}){3,4}$/.test(hex)) {
const rgb = [parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16)];
this.hsva = _rgb2hsv(rgb).concat([this.hsva[3]]);
this.colorSelectedAnchor(this.color);
}
}
}
_onMouseDown(evt) {
if (evt.currentTarget === this._colorRect.dom) {
this._dragMode = 1; // drag color
}
else if (evt.currentTarget === this._hueRect.dom) {
this._dragMode = 2; // drag hue
}
else {
this._dragMode = 3; // drag alpha
}
this._storeHsva = this._hsva.slice();
this._onMouseMove(evt);
// hook up mouse
window.addEventListener('mousemove', this.moveHandler);
window.addEventListener('mouseup', this.upHandler);
}
_onMouseMove(evt) {
let newhsva;
if (this._dragMode === 1) {
const m = this.Helpers.normalizedCoord(this._colorRect, evt.pageX, evt.pageY);
const s = math.clamp(m[0], 0, 1);
const v = math.clamp(m[1], 0, 1);
newhsva = [this.hsva[0], s, 1 - v, this._hsva[3]];
}
else if (this._dragMode === 2) {
const m = this.Helpers.normalizedCoord(this._hueRect, evt.pageX, evt.pageY);
const h = math.clamp(m[1], 0, 1);
newhsva = [h, this.hsva[1], this.hsva[2], this.hsva[3]];
}
else {
const m = this.Helpers.normalizedCoord(this._alphaRect, evt.pageX, evt.pageY);
const a = math.clamp(m[1], 0, 1);
newhsva = [this.hsva[0], this.hsva[1], this.hsva[2], 1 - a];
}
if (newhsva[0] !== this._hsva[0] ||
newhsva[1] !== this._hsva[1] ||
newhsva[2] !== this._hsva[2] ||
newhsva[3] !== this._hsva[3]) {
this.hsva = newhsva;
this.emit('changing', this.color);
}
}
_onMouseUp(evt) {
window.removeEventListener('mousemove', this.moveHandler);
window.removeEventListener('mouseup', this.upHandler);
if (this._storeHsva[0] !== this._hsva[0] ||
this._storeHsva[1] !== this._hsva[1] ||
this._storeHsva[2] !== this._hsva[2] ||
this._storeHsva[3] !== this._hsva[3]) {
this.colorSelectedAnchor(this.color);
}
}
set hsva(hsva) {
const rgb = _hsv2rgb(hsva);
const hueRgb = _hsv2rgb([hsva[0], 1, 1]);
// regenerate gradient canvas if hue changes
if (hsva[0] !== this._hsva[0]) {
this._generateGradient(this._colorRect, hueRgb);
}
const e = this._colorRect.dom;
const r = e.getBoundingClientRect();
const w = r.width - 2;
const h = r.height - 2;
this._colorHandle.style.backgroundColor = this.Helpers.rgbaStr(rgb);
this._colorHandle.style.left = `${e.offsetLeft - 7 + Math.floor(w * hsva[1])}px`;
this._colorHandle.style.top = `${e.offsetTop - 7 + Math.floor(h * (1 - hsva[2]))}px`;
this._hueHandle.style.backgroundColor = this.Helpers.rgbaStr(hueRgb);
this._hueHandle.style.top = `${e.offsetTop - 3 + Math.floor(140 * hsva[0])}px`;
this._hueHandle.style.left = '162px';
this._alphaHandle.style.backgroundColor = this.Helpers.rgbaStr(_hsv2rgb([0, 0, hsva[3]]));
this._alphaHandle.style.top = `${e.offsetTop - 3 + Math.floor(140 * (1 - hsva[3]))}px`;
this._alphaHandle.style.left = '194px';
this._changing = true;
this._rField.value = rgb[0];
this._gField.value = rgb[1];
this._bField.value = rgb[2];
this._aField.value = Math.round(hsva[3] * 255);
this._hexField.value = this.Helpers.hexStr(rgb);
this._changing = false;
this._hsva = hsva;
}
get hsva() {
return this._hsva;
}
set color(clr) {
const hsva = this.Helpers.toHsva(clr);
if (hsva[0] === 0 && hsva[1] === 0 && this._hsva[0] !== -1) {
// if the incoming RGB is a shade of grey (without hue),
// use the current active hue instead.
hsva[0] = this._hsva[0];
}
this.hsva = hsva;
}
get color() {
return this.Helpers.toRgba(this._hsva);
}
set editAlpha(editAlpha) {
if (editAlpha) {
this._alphaRect.dom.style.display = 'inline';
this._alphaHandle.style.display = 'block';
this._aField.dom.style.display = 'inline-block';
}
else {
this._alphaRect.dom.style.display = 'none';
this._alphaHandle.style.display = 'none';
this._aField.dom.style.display = 'none';
}
}
get editAlpha() {
return this.editAlpha;
}
// open the picker
open() {
this.UI.overlay.hidden = false;
}
// close the picker
close() {
this.UI.overlay.hidden = true;
}
// handle the picker being opened
onOpen() {
window.addEventListener('mousemove', this._onAnchorsMouseMove);
window.addEventListener('mouseup', this._onAnchorsMouseUp);
this.UI.anchors.dom.addEventListener('mousedown', this._onAnchorsMouseDown);
// editor.emit('picker:gradient:open');
// editor.emit('picker:open', 'gradient');
}
// handle the picker being closed
onClose() {
this.STATE.hoveredAnchor = -1;
window.removeEventListener('mousemove', this._onAnchorsMouseMove);
window.removeEventListener('mouseup', this._onAnchorsMouseUp);
this.UI.anchors.dom.removeEventListener('mousedown', this._onAnchorsMouseDown);
this._evtRefreshPicker.unbind();
this._evtRefreshPicker = null;
this._evtPickerChanged.unbind();
this._evtPickerChanged = null;
}
onDeleteKey() {
if (!this.UI.overlay.hidden) {
if (this.STATE.selectedAnchor !== -1) {
const deleteTime = this.STATE.anchors[this.STATE.selectedAnchor];
this.STATE.selectedAnchor = -1;
this.deleteAnchor(deleteTime);
}
}
}
_onTypeChanged(value) {
value = this.STATE.typeMap[value];
const paths = [];
const values = [];
for (let i = 0; i < this.STATE.curves.length; ++i) {
paths.push(`${i.toString()}.type`);
values.push(value);
}
this.emit('picker:curve:change', paths, values);
}
render() {
this.renderGradient();
this.renderAnchors();
}
renderGradient() {
const ctx = this.UI.gradient.dom.getContext('2d');
const w = this.UI.gradient.width;
const h = this.UI.gradient.height;
const r = this.UI.gradient.pixelRatio;
ctx.setTransform(r, 0, 0, r, 0, 0);
// fill background
ctx.fillStyle = this.UI.checkerPattern;
ctx.fillRect(0, 0, w, h);
// fill gradient
const gradient = ctx.createLinearGradient(0, 0, w, 0);
for (let t = 0; t <= w; t += 2) {
const x = t / w;
gradient.addColorStop(x, this.Helpers.rgbaStr(this.evaluateGradient(x), 255));
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
// render the tip of the selected anchor
if (this.STATE.selectedAnchor !== -1) {
const time = this.STATE.anchors[this.STATE.selectedAnchor];
const coords = [time * w, h];
const rectHeight = this.UI.draggingAnchor ? -30 : -6;
ctx.beginPath();
ctx.rect(coords[0] - 0.5, coords[1], 1, rectHeight);
ctx.fillStyle = 'rgb(41, 53, 56)';
ctx.fill();
}
}
renderAnchors() {
const ctx = this.UI.anchors.dom.getContext('2d');
const w = this.UI.anchors.width;
const h = this.UI.anchors.height;
const r = this.UI.anchors.pixelRatio;
ctx.setTransform(r, 0, 0, r, 0, 0);
ctx.fillStyle = this.CONSTANTS.bg;
ctx.fillRect(0, 0, w, h);
// render plain anchors
for (let index = 0; index < this.STATE.anchors.length; ++index) {
if (index !== this.STATE.hoveredAnchor &&
index !== this.STATE.selectedAnchor) {
this.renderAnchor(ctx, this.STATE.anchors[index]);
}
}
if ((this.STATE.hoveredAnchor !== -1) &&
(this.STATE.hoveredAnchor !== this.STATE.selectedAnchor)) {
this.renderAnchor(ctx, this.STATE.anchors[this.STATE.hoveredAnchor], 'hovered');
}
if (this.STATE.selectedAnchor !== -1) {
this.renderAnchor(ctx, this.STATE.anchors[this.STATE.selectedAnchor], 'selected');
}
}
renderAnchor(ctx, time, type) {
const coords = [time * this.UI.anchors.width, this.UI.anchors.height / 2];
const radius = (type === 'selected' ? this.CONSTANTS.selectedRadius : this.CONSTANTS.anchorRadius);
// render selected arrow
if (type === 'selected') {
ctx.beginPath();
ctx.rect(coords[0] - 0.5, coords[1], 1, -coords[1]);
ctx.fillStyle = 'rgb(41, 53, 56)';
ctx.fill();
}
// render selection highlight
if (type === 'selected' || type === 'hovered') {
ctx.beginPath();
ctx.arc(coords[0], coords[1], (radius + 2), 0, 2 * Math.PI, false);
ctx.fillStyle = 'rgb(255, 255, 255)';
ctx.fill();
}
ctx.beginPath();
ctx.arc(coords[0], coords[1], (radius + 1), 0, 2 * Math.PI, false);
ctx.fillStyle = this.Helpers.rgbaStr(this.evaluateGradient(time, 1), 255);
ctx.fill();
}
evaluateGradient(time, alphaOverride) {
const result = [];
for (let i = 0; i < 3; ++i) {
result.push(this.STATE.curves[i].value(time));
}
if (alphaOverride) {
result.push(alphaOverride);
}
else if (this.STATE.curves.length > 3) {
result.push(this.STATE.curves[3].value(time));
}
else {
result.push(1);
}
return result;
}
calcAnchorTimes() {
// get curve anchor points
let times = [];
for (let i = 0; i < this.STATE.curves.length; i++) {
const curve = this.STATE.curves[i];
for (let j = 0; j < curve.keys.length; ++j) {
times.push(curve.keys[j][0]);
}
}
// sort anchors and remove duplicates
times.sort();
times = times.filter((item, pos, ary) => {
return !pos || item !== ary[pos - 1];
});
return times;
}
// helper function for calculating the normalized coordinate
// x,y relative to rect
calcNormalizedCoord(x, y, rect) {
return [(x - rect.left) / rect.width,
(y - rect.top) / rect.height];
}
// get the bounding client rect minus padding
getClientRect(element) {
const styles = window.getComputedStyle(element);
const paddingTop = parseFloat(styles.paddingTop);
const paddingRight = parseFloat(styles.paddingRight);
const paddingBottom = parseFloat(styles.paddingBottom);
const paddingLeft = parseFloat(styles.paddingLeft);
const rect = element.getBoundingClientRect();
return new DOMRect(rect.x + paddingLeft, rect.y + paddingTop, rect.width - paddingRight - paddingLeft, rect.height - paddingTop - paddingBottom);
}
selectHovered(index) {
this.STATE.hoveredAnchor = index;
this.UI.anchors.dom.style.cursor = (index === -1 ? '' : 'pointer');
}
selectAnchor(index) {
this.STATE.selectedAnchor = index;
this.STATE.changing = true;
if (index === -1) {
this.UI.positionEdit.value = '';
this.color = [0, 0, 0];
}
else {
const time = this.STATE.anchors[index];
this.UI.positionEdit.value = Math.round(time * 100);
this.STATE.selectedValue = this.evaluateGradient(time);
this.color = this.STATE.selectedValue;
this.UI.showSelectedPosition.dom.style.left = `${(this.STATE.anchors[index] * this.UI.gradient.width - 4).toString()}px`;
this.UI.showSelectedPosition.value = Math.round(this.STATE.anchors[index] * 100);
}
this.STATE.changing = false;
this.render();
}
dragStart() {
if (this.STATE.selectedAnchor === -1) {
return;
}
const time = this.STATE.anchors[this.STATE.selectedAnchor];
// make a copy of the curve data before editing starts
this.STATE.keystore = [];
for (let i = 0; i < this.STATE.curves.length; ++i) {
const keys = [];
this.STATE.curves[i].keys.forEach((element) => {
if (element[0] !== time) {
keys.push([element[0], element[1]]);
}
});
this.STATE.keystore.push(keys);
}
}
dragUpdate(time) {
if (this.STATE.selectedAnchor === -1) {
return;
}
for (let i = 0; i < this.STATE.curves.length; ++i) {
const curve = this.STATE.curves[i];
const keystore = this.STATE.keystore[i];
// merge keystore with the drag anchor (ignoring existing anchors at
// the current anchor location)
curve.keys = keystore.map((element) => {
return [element[0], element[1]];
})
.filter((element) => {
return element[0] !== time;
});
curve.keys.push([time, this.STATE.selectedValue[i]]);
curve.sort();
}
this.STATE.anchors = this.calcAnchorTimes();
this.selectAnchor(this.STATE.anchors.indexOf(time));
}
dragEnd() {
if (this.STATE.selectedAnchor !== -1) {
this.emitCurveChange();
}
}
// insert an anchor at the given time with the given color
insertAnchor(time, color) {
for (let i = 0; i < this.STATE.curves.length; ++i) {
const keys = this.STATE.curves[i].keys;
let j = 0;
while (j < keys.length) {
if (keys[j][0] >= time) {
break;
}
++j;
}
if (j < keys.length && keys[j][0] === time) {
keys[j][1] = color[i];
}
else {
keys.splice(j, 0, [time, color[i]]);
}
}
this.emitCurveChange();
}
// delete the anchor(s) at the given time
deleteAnchor(time) {
for (let i = 0; i < this.STATE.curves.length; ++i) {
const curve = this.STATE.curves[i];
for (let j = 0; j < curve.keys.length; ++j) {
if (curve.keys[j][0] === time) {
curve.keys.splice(j, 1);
break;
}
}
}
this.selectHovered(-1);
this.emitCurveChange();
}
moveSelectedAnchor(time) {
if (this.STATE.selectedAnchor !== -1) {
this.dragStart();
this.dragUpdate(time);
this.dragEnd();
}
}
colorSelectedAnchor(clr, dragging) {
if (this.STATE.selectedAnchor !== -1) {
const time = this.STATE.anchors[this.STATE.selectedAnchor];
for (let i = 0; i < this.STATE.curves.length; ++i) {
const curve = this.STATE.curves[i];
for (let j = 0; j < curve.keys.length; ++j) {
if (curve.keys[j][0] === time) {
curve.keys[j][1] = clr[i];
break;
}
}
}
this.STATE.selectedValue = clr;
if (dragging) {
this.render();
}
else {
this.emitCurveChange();
}
}
}
emitCurveChange() {
const paths = [];
const values = [];
this.STATE.curves.forEach((curve, index) => {
paths.push(`0.keys.${index}`);
const keys = [];
curve.keys.forEach((key) => {
keys.push(key[0], key[1]);
});
values.push(keys);
});
this.emit('picker:curve:change', paths, values);
}
doCopy() {
const data = {
type: this.STATE.curves[0].type,
keys: this.STATE.curves.map((c) => {
return [].concat(...c.keys);
})
};
this._copiedData = data;
}
doPaste() {
const data = this._copiedData;
if (data !== null) {
// only paste the number of curves we're currently editing
const pasteData = {
type: data.type,
keys: []
};
for (let index = 0; index < this.STATE.curves.length; ++index) {
if (index < data.keys.length) {
pasteData.keys.push(data.keys[index]);
}
else {
pasteData.keys.push([].concat(...this.STATE.curves[index].keys));
}
}
this.setValue([pasteData]);
this.emitCurveChange();
}
}
doDelete() {
const toDelete = this.STATE.selectedAnchor;
if (toDelete === -1 || this.STATE.curves[0].keys.length === 1) {
return;
}
for (let i = 0; i < this.STATE.curves.length; ++i) {
const keys = this.STATE.curves[i].keys;
keys.splice(toDelete, 1);
}
this.emitCurveChange();
}
createCheckerPattern() {
const canvas = new Canvas();
canvas.width = 16;
canvas.height = 16;
const canvasElement = canvas.dom;
const ctx = canvasElement.getContext('2d');
ctx.fillStyle = '#949a9c';
ctx.fillRect(0, 0, 8, 8);
ctx.fillRect(8, 8, 8, 8);
ctx.fillStyle = '#657375';
ctx.fillRect(8, 0, 8, 8);
ctx.fillRect(0, 8, 8, 8);
return ctx.createPattern(canvasElement, 'repeat');
}
setValue(value, args) {
// sanity checks mostly for script 'curve' attributes
if (!(value instanceof Array) ||
value.length !== 1 ||
value[0].keys === undefined ||
(value[0].keys.length !== 3 && value[0].keys.length !== 4)) {
return;
}
this.STATE.typeMap = {
0: CURVE_STEP,
1: CURVE_LINEAR,
2: CURVE_SPLINE
};
const indexMap = Object.fromEntries(Object
.entries(this.STATE.typeMap)
.map(([key, value]) => [value, key]));
// check if curve is using a legacy curve type
if (value[0].type !== CURVE_STEP &&
value[0].type !== CURVE_LINEAR &&
value[0].type !== CURVE_SPLINE) {
this.STATE.typeMap[3] = value[0].type;
}
this.UI.typeCombo.options = [{ v: 0, t: 'Step' }, { v: 1, t: 'Linear' }, { v: 2, t: 'Spline' }];
this.UI.typeCombo.value = this.UI.typeCombo.value === -1 ? indexMap[this.value.type] : this.UI.typeCombo.value;
// store the curves
this.STATE.curves = [];
value[0].keys.forEach((keys) => {
const curve = new Curve(keys);
curve.type = value[0].type;
this.STATE.curves.push(curve);
});
// calculate the anchor times
this.STATE.anchors = this.calcAnchorTimes();
// select the anchor
if (this.STATE.anchors.length === 0) {
this.selectAnchor(-1);
}
else {
this.selectAnchor(math.clamp(this.STATE.selectedAnchor, 0, this.STATE.anchors.length - 1));
}
this.editAlpha = this.STATE.curves.length > 3;
}
callOpenGradientPicker(value, args) {
this.setValue(value, args);
this.open();
}
getGradientPickerRect() {
return this.UI.overlay.dom.getBoundingClientRect();
}
positionGradientPicker(x, y) {
if (y + this.UI.panel.clientHeight > window.innerHeight) {
y = window.innerHeight - this.UI.panel.clientHeight;
}
this.UI.overlay.position(x, y);
}
setGradientPicker(value, args) {
this.setValue(value, args);
}
}
Element.register('div', GradientPicker);
export { GradientPicker };
//# sourceMappingURL=index.mjs.map