Eduard Kuzmenko
4 years ago
14 changed files with 673 additions and 69 deletions
@ -0,0 +1,336 @@
@@ -0,0 +1,336 @@
|
||||
import { ColorHsla, hexaToHsla, hslaToRgba, rgbaToHexa as rgbaToHexa, rgbaToHsla } from "../helpers/color"; |
||||
import { clamp } from "../helpers/number"; |
||||
import InputField, { InputState } from "./inputField"; |
||||
|
||||
type EventPosition = {x: number, y: number, isTouch?: boolean}; |
||||
export type ColorPickerColor = { |
||||
hsl: string; |
||||
rgb: string; |
||||
hex: string; |
||||
hsla: string; |
||||
rgba: string; |
||||
hexa: string; |
||||
rgbaArray: import("f:/tweb/src/helpers/color").ColorRgba; |
||||
}; |
||||
|
||||
export default class ColorPicker { |
||||
private static BASE_CLASS = 'color-picker'; |
||||
public container: HTMLElement; |
||||
|
||||
private boxRect: DOMRect; |
||||
//private boxDraggerRect: DOMRect;
|
||||
private hueRect: DOMRect; |
||||
//private hueDraggerRect: DOMRect;
|
||||
|
||||
private hue = 0; |
||||
private saturation = 100; |
||||
private lightness = 50; |
||||
private alpha = 1; |
||||
private elements: { |
||||
box: SVGSVGElement, |
||||
boxDragger: SVGSVGElement, |
||||
sliders: HTMLElement, |
||||
hue: SVGSVGElement, |
||||
hueDragger: SVGSVGElement, |
||||
saturation: SVGLinearGradientElement, |
||||
} = {} as any; |
||||
private hexInputField: InputField; |
||||
private rgbInputField: InputField; |
||||
public onChange: (color: ReturnType<ColorPicker['getCurrentColor']>) => void; |
||||
|
||||
constructor() { |
||||
this.container = document.createElement('div'); |
||||
this.container.classList.add(ColorPicker.BASE_CLASS); |
||||
|
||||
const html = ` |
||||
<svg class="${ColorPicker.BASE_CLASS + '-box'}" viewBox="0 0 380 198"> |
||||
<defs> |
||||
<linearGradient id="color-picker-saturation" x1="0%" y1="0%" x2="100%" y2="0%"> |
||||
<stop offset="0%" stop-color="#fff"></stop> |
||||
<stop offset="100%" stop-color="hsl(0,100%,50%)"></stop> |
||||
</linearGradient> |
||||
<linearGradient id="color-picker-brightness" x1="0%" y1="0%" x2="0%" y2="100%"> |
||||
<stop offset="0%" stop-color="rgba(0,0,0,0)"></stop> |
||||
<stop offset="100%" stop-color="#000"></stop> |
||||
</linearGradient> |
||||
<pattern id="color-picker-pattern" width="100%" height="100%"> |
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#color-picker-saturation)"></rect> |
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#color-picker-brightness)"></rect> |
||||
</pattern> |
||||
</defs> |
||||
<rect rx="10" ry="10" x="0" y="0" width="380" height="198" fill="url(#color-picker-pattern)"></rect> |
||||
<svg class="${ColorPicker.BASE_CLASS + '-dragger'} ${ColorPicker.BASE_CLASS + '-box-dragger'}" x="0" y="0"> |
||||
<circle r="11" fill="inherit" stroke="#fff" stroke-width="2"></circle> |
||||
</svg> |
||||
</svg> |
||||
<div class="${ColorPicker.BASE_CLASS + '-sliders'}"> |
||||
<svg class="${ColorPicker.BASE_CLASS + '-color-slider'}" viewBox="0 0 380 24"> |
||||
<defs> |
||||
<linearGradient id="hue" x1="100%" y1="0%" x2="0%" y2="0%"> |
||||
<stop offset="0%" stop-color="#f00"></stop> |
||||
<stop offset="16.666%" stop-color="#f0f"></stop> |
||||
<stop offset="33.333%" stop-color="#00f"></stop> |
||||
<stop offset="50%" stop-color="#0ff"></stop> |
||||
<stop offset="66.666%" stop-color="#0f0"></stop> |
||||
<stop offset="83.333%" stop-color="#ff0"></stop> |
||||
<stop offset="100%" stop-color="#f00"></stop> |
||||
</linearGradient> |
||||
</defs> |
||||
<rect rx="4" ry="4" x="0" y="9" width="380" height="8" fill="url(#hue)"></rect> |
||||
<svg class="${ColorPicker.BASE_CLASS + '-dragger'} ${ColorPicker.BASE_CLASS + '-color-slider-dragger'}" x="0" y="13"> |
||||
<circle r="11" fill="inherit" stroke="#fff" stroke-width="2"></circle> |
||||
</svg> |
||||
</svg> |
||||
</div> |
||||
`;
|
||||
|
||||
this.container.innerHTML = html; |
||||
|
||||
this.elements.box = this.container.firstElementChild as any; |
||||
this.elements.boxDragger = this.elements.box.lastElementChild as any; |
||||
this.elements.saturation = this.elements.box.firstElementChild.firstElementChild as any; |
||||
|
||||
this.elements.sliders = this.elements.box.nextElementSibling as any; |
||||
|
||||
this.elements.hue = this.elements.sliders.firstElementChild as any; |
||||
this.elements.hueDragger = this.elements.hue.lastElementChild as any; |
||||
|
||||
this.hexInputField = new InputField({plainText: true, label: 'Appearance.Color.Hex'}); |
||||
this.rgbInputField = new InputField({plainText: true, label: 'Appearance.Color.RGB'}); |
||||
|
||||
const inputs = document.createElement('div'); |
||||
inputs.className = ColorPicker.BASE_CLASS + '-inputs'; |
||||
inputs.append(this.hexInputField.container, this.rgbInputField.container); |
||||
this.container.append(inputs); |
||||
|
||||
this.hexInputField.input.addEventListener('input', () => { |
||||
let value = this.hexInputField.value.replace(/#/g, '').slice(0, 6); |
||||
|
||||
const match = value.match(/([a-fA-F\d]+)/); |
||||
const valid = match && match[0].length === value.length && [/* 3, 4, */6].includes(value.length); |
||||
this.hexInputField.setState(valid ? InputState.Neutral : InputState.Error); |
||||
|
||||
value = '#' + value; |
||||
this.hexInputField.setValueSilently(value); |
||||
|
||||
if(valid) { |
||||
this.setColor(value, false, true); |
||||
} |
||||
}); |
||||
|
||||
// patched https://stackoverflow.com/a/34029238/6758968
|
||||
const rgbRegExp = /^(?:rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(?:\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(?:([01]?\d\d?|2[0-4]\d|25[0-5])\)?)$/; |
||||
this.rgbInputField.input.addEventListener('input', () => { |
||||
const match = this.rgbInputField.value.match(rgbRegExp); |
||||
this.rgbInputField.setState(match ? InputState.Neutral : InputState.Error); |
||||
|
||||
if(match) { |
||||
this.setColor(rgbaToHsla(+match[1], +match[2], +match[3]), true, false); |
||||
} |
||||
}); |
||||
|
||||
this.attachBoxListeners(); |
||||
this.attachHueListeners(); |
||||
} |
||||
|
||||
private attachGrabListeners(element: SVGSVGElement, onStart: (position: EventPosition) => void, onMove: (position: EventPosition) => void, onEnd: (position: EventPosition) => void) { |
||||
// * Mouse
|
||||
const onMouseMove = (event: MouseEvent) => { |
||||
onMove({x: event.pageX, y: event.pageY}); |
||||
}; |
||||
|
||||
const onMouseDown = (event: MouseEvent) => { |
||||
if(event.button !== 0) { |
||||
element.addEventListener('mousedown', onMouseDown, {once: true}); |
||||
return; |
||||
} |
||||
|
||||
this.onGrabStart(); |
||||
onStart({x: event.pageX, y: event.pageY}); |
||||
onMouseMove(event); |
||||
|
||||
document.addEventListener('mousemove', onMouseMove); |
||||
document.addEventListener('mouseup', () => { |
||||
document.removeEventListener('mousemove', onMouseMove); |
||||
element.addEventListener('mousedown', onMouseDown, {once: true}); |
||||
this.onGrabEnd(); |
||||
onEnd && onEnd({x: event.pageX, y: event.pageY}); |
||||
}, {once: true}); |
||||
}; |
||||
|
||||
element.addEventListener('mousedown', onMouseDown, {once: true}); |
||||
|
||||
// * Touch
|
||||
const onTouchMove = (event: TouchEvent) => { |
||||
event.preventDefault(); |
||||
onMove({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true}); |
||||
}; |
||||
|
||||
const onTouchStart = (event: TouchEvent) => { |
||||
this.onGrabStart(); |
||||
onStart({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true}); |
||||
onTouchMove(event); |
||||
|
||||
document.addEventListener('touchmove', onTouchMove, {passive: false}); |
||||
document.addEventListener('touchend', (event) => { |
||||
document.removeEventListener('touchmove', onTouchMove); |
||||
element.addEventListener('touchstart', onTouchStart, {passive: true, once: true}); |
||||
this.onGrabEnd(); |
||||
onEnd && onEnd({x: event.touches[0].clientX, y: event.touches[0].clientY, isTouch: true}); |
||||
}, {passive: true, once: true}); |
||||
}; |
||||
|
||||
element.addEventListener('touchstart', onTouchStart, {passive: true, once: true}); |
||||
} |
||||
|
||||
private onGrabStart = () => { |
||||
document.documentElement.style.cursor = this.elements.boxDragger.style.cursor = 'grabbing'; |
||||
}; |
||||
|
||||
private onGrabEnd = () => { |
||||
document.documentElement.style.cursor = this.elements.boxDragger.style.cursor = ''; |
||||
}; |
||||
|
||||
private attachBoxListeners() { |
||||
this.attachGrabListeners(this.elements.box, () => { |
||||
this.boxRect = this.elements.box.getBoundingClientRect(); |
||||
//this.boxDraggerRect = this.elements.boxDragger.getBoundingClientRect();
|
||||
}, (pos) => { |
||||
this.saturationHandler(pos.x, pos.y); |
||||
}, null); |
||||
} |
||||
|
||||
private attachHueListeners() { |
||||
this.attachGrabListeners(this.elements.hue, () => { |
||||
this.hueRect = this.elements.hue.getBoundingClientRect(); |
||||
//this.hueDraggerRect = this.elements.hueDragger.getBoundingClientRect();
|
||||
}, (pos) => { |
||||
this.hueHandler(pos.x); |
||||
}, null); |
||||
} |
||||
|
||||
public setColor(color: ColorHsla | string, updateHexInput = true, updateRgbInput = true) { |
||||
if(color === undefined) { // * set to red
|
||||
color = { |
||||
h: 0, |
||||
s: 100, |
||||
l: 50, |
||||
a: 1 |
||||
}; |
||||
} else if(typeof(color) === 'string') { |
||||
if(color[0] === '#') { |
||||
color = hexaToHsla(color); |
||||
} else { |
||||
const rgb = color.match(/[.?\d]+/g); |
||||
color = rgbaToHsla(+rgb[0], +rgb[1], +rgb[2], rgb[3] === undefined ? 1 : +rgb[3]); |
||||
} |
||||
} |
||||
|
||||
// Set box
|
||||
this.boxRect = this.elements.box.getBoundingClientRect(); |
||||
|
||||
const boxX = this.boxRect.width / 100 * color.s; |
||||
const percentY = 100 - (color.l / (100 - color.s / 2)) * 100; |
||||
const boxY = this.boxRect.height / 100 * percentY; |
||||
|
||||
this.saturationHandler(this.boxRect.left + boxX, this.boxRect.top + boxY, false); |
||||
|
||||
// Set hue
|
||||
this.hueRect = this.elements.hue.getBoundingClientRect(); |
||||
|
||||
const percentHue = color.h / 360; |
||||
const hueX = this.hueRect.left + this.hueRect.width * percentHue; |
||||
|
||||
this.hueHandler(hueX, false); |
||||
|
||||
// Set values
|
||||
this.hue = color.h; |
||||
this.saturation = color.s; |
||||
this.lightness = color.l; |
||||
this.alpha = color.a; |
||||
|
||||
this.updatePicker(updateHexInput, updateRgbInput); |
||||
}; |
||||
|
||||
public getCurrentColor(): ColorPickerColor { |
||||
const rgbaArray = hslaToRgba(this.hue, this.saturation, this.lightness, this.alpha); |
||||
const hexa = rgbaToHexa(rgbaArray); |
||||
const hex = hexa.slice(0, -2); |
||||
|
||||
return { |
||||
hsl: `hsl(${this.hue}, ${this.saturation}%, ${this.lightness}%)`, |
||||
rgb: `rgb(${rgbaArray[0]}, ${rgbaArray[1]}, ${rgbaArray[2]})`, |
||||
hex: hex, |
||||
hsla: `hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha})`, |
||||
rgba: `rgba(${rgbaArray[0]}, ${rgbaArray[1]}, ${rgbaArray[2]}, ${rgbaArray[3]})`, |
||||
hexa: hexa, |
||||
rgbaArray: rgbaArray |
||||
}; |
||||
} |
||||
|
||||
public updatePicker(updateHexInput = true, updateRgbInput = true) { |
||||
const color = this.getCurrentColor(); |
||||
this.elements.boxDragger.setAttributeNS(null, 'fill', color.hex); |
||||
|
||||
if(updateHexInput) { |
||||
this.hexInputField.setValueSilently(color.hex); |
||||
this.hexInputField.setState(InputState.Neutral); |
||||
} |
||||
|
||||
if(updateRgbInput) { |
||||
this.rgbInputField.setValueSilently(color.rgbaArray.slice(0, -1).join(', ')); |
||||
this.rgbInputField.setState(InputState.Neutral); |
||||
} |
||||
|
||||
if(this.onChange) { |
||||
this.onChange(color); |
||||
} |
||||
} |
||||
|
||||
private hueHandler(pageX: number, update = true) { |
||||
const eventX = clamp(pageX - this.hueRect.left, 0, this.hueRect.width); |
||||
|
||||
const percents = eventX / this.hueRect.width; |
||||
this.hue = Math.round(360 * percents); |
||||
|
||||
const hsla = `hsla(${this.hue}, 100%, 50%, ${this.alpha})`; |
||||
|
||||
this.elements.hueDragger.setAttributeNS(null, 'x', (percents * 100) + '%'); |
||||
this.elements.hueDragger.setAttributeNS(null, 'fill', hsla); |
||||
|
||||
this.elements.saturation.lastElementChild.setAttributeNS(null, 'stop-color', hsla); |
||||
|
||||
if(update) { |
||||
this.updatePicker(); |
||||
} |
||||
} |
||||
|
||||
private saturationHandler(pageX: number, pageY: number, update = true) { |
||||
const maxX = this.boxRect.width; |
||||
const maxY = this.boxRect.height; |
||||
|
||||
const eventX = clamp(pageX - this.boxRect.left, 0, maxX); |
||||
const eventY = clamp(pageY - this.boxRect.top, 0, maxY); |
||||
|
||||
const posX = eventX / maxX * 100; |
||||
const posY = eventY / maxY * 100; |
||||
|
||||
const boxDragger = this.elements.boxDragger; |
||||
boxDragger.setAttributeNS(null, 'x', posX + '%'); |
||||
boxDragger.setAttributeNS(null, 'y', posY + '%'); |
||||
|
||||
const saturation = clamp(posX, 0, 100); |
||||
|
||||
const lightnessX = 100 - saturation / 2; |
||||
const lightnessY = 100 - clamp(posY, 0, 100); |
||||
|
||||
const lightness = clamp(lightnessY / 100 * lightnessX, 0, 100); |
||||
|
||||
this.saturation = saturation; |
||||
this.lightness = lightness; |
||||
|
||||
if(update) { |
||||
this.updatePicker(); |
||||
} |
||||
}; |
||||
} |
@ -0,0 +1,142 @@
@@ -0,0 +1,142 @@
|
||||
import { SettingSection } from ".."; |
||||
import { hexaToRgba } from "../../../helpers/color"; |
||||
import { attachClickEvent } from "../../../helpers/dom"; |
||||
import findUpClassName from "../../../helpers/dom/findUpClassName"; |
||||
import highlightningColor from "../../../helpers/highlightningColor"; |
||||
import { throttle } from "../../../helpers/schedulers"; |
||||
import appImManager from "../../../lib/appManagers/appImManager"; |
||||
import appStateManager from "../../../lib/appManagers/appStateManager"; |
||||
import rootScope from "../../../lib/rootScope"; |
||||
import ColorPicker, { ColorPickerColor } from "../../colorPicker"; |
||||
import { SliderSuperTab } from "../../slider"; |
||||
|
||||
export default class AppBackgroundColorTab extends SliderSuperTab { |
||||
private colorPicker: ColorPicker; |
||||
private grid: HTMLElement; |
||||
private applyColor: (hex: string, updateColorPicker?: boolean) => void; |
||||
|
||||
init() { |
||||
this.container.classList.add('background-container', 'background-color-container'); |
||||
this.setTitle('SetColor'); |
||||
|
||||
const section = new SettingSection({}); |
||||
this.colorPicker = new ColorPicker(); |
||||
|
||||
section.content.append(this.colorPicker.container); |
||||
|
||||
this.scrollable.append(section.container); |
||||
|
||||
const grid = this.grid = document.createElement('div'); |
||||
grid.classList.add('grid'); |
||||
|
||||
const colors = [ |
||||
'#E6EBEE', |
||||
'#B2CEE1', |
||||
'#008DD0', |
||||
'#C6E7CB', |
||||
'#C4E1A6', |
||||
'#60B16E', |
||||
'#CCD0AF', |
||||
'#A6A997', |
||||
'#7A7072', |
||||
'#FDD7AF', |
||||
'#FDB76E', |
||||
'#DD8851' |
||||
]; |
||||
|
||||
colors.forEach(color => { |
||||
const item = document.createElement('div'); |
||||
item.classList.add('grid-item'); |
||||
item.dataset.color = color.toLowerCase(); |
||||
|
||||
// * need for transform scale
|
||||
const media = document.createElement('div'); |
||||
media.classList.add('grid-item-media'); |
||||
media.style.backgroundColor = color; |
||||
|
||||
item.append(media); |
||||
grid.append(item); |
||||
}); |
||||
|
||||
attachClickEvent(grid, (e) => { |
||||
const target = findUpClassName(e.target, 'grid-item'); |
||||
if(!target || target.classList.contains('active')) { |
||||
return; |
||||
} |
||||
|
||||
const color = target.dataset.color; |
||||
if(!color) { |
||||
return; |
||||
} |
||||
|
||||
this.applyColor(color); |
||||
}, {listenerSetter: this.listenerSetter}); |
||||
|
||||
this.scrollable.append(grid); |
||||
|
||||
this.applyColor = throttle(this._applyColor, 16, true); |
||||
} |
||||
|
||||
private setActive() { |
||||
const active = this.grid.querySelector('.active'); |
||||
const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background; |
||||
const target = background.type === 'color' ? this.grid.querySelector(`.grid-item[data-color="${background.color}"]`) : null; |
||||
if(active === target) { |
||||
return; |
||||
} |
||||
|
||||
if(active) { |
||||
active.classList.remove('active'); |
||||
} |
||||
|
||||
if(target) { |
||||
target.classList.add('active'); |
||||
} |
||||
} |
||||
|
||||
private _applyColor = (hex: string, updateColorPicker = true) => { |
||||
if(updateColorPicker) { |
||||
this.colorPicker.setColor(hex); |
||||
} else { |
||||
const rgba = hexaToRgba(hex); |
||||
const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background; |
||||
const hsla = highlightningColor(rgba); |
||||
|
||||
background.color = hex.toLowerCase(); |
||||
background.type = 'color'; |
||||
background.highlightningColor = hsla; |
||||
appStateManager.pushToState('settings', rootScope.settings); |
||||
|
||||
appImManager.applyCurrentTheme(undefined, undefined, true); |
||||
this.setActive(); |
||||
} |
||||
}; |
||||
|
||||
private onColorChange = (color: ColorPickerColor) => { |
||||
this.applyColor(color.hex, false); |
||||
}; |
||||
|
||||
onOpen() { |
||||
setTimeout(() => { |
||||
const background = rootScope.settings.themes.find(t => t.name === rootScope.settings.theme).background; |
||||
|
||||
// * set active if type is color
|
||||
if(background.type === 'color') { |
||||
this.colorPicker.onChange = this.onColorChange; |
||||
} |
||||
|
||||
this.colorPicker.setColor(background.color || '#cccccc'); |
||||
|
||||
if(background.type !== 'color') { |
||||
this.colorPicker.onChange = this.onColorChange; |
||||
} |
||||
}, 0); |
||||
} |
||||
|
||||
onCloseAfterTimeout() { |
||||
this.colorPicker.onChange = undefined; |
||||
this.colorPicker = undefined; |
||||
|
||||
return super.onCloseAfterTimeout(); |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
import { rgbaToHsla } from "./color"; |
||||
|
||||
// * https://github.com/TelegramMessenger/Telegram-iOS/blob/3d062fff78cc6b287c74e6171f855a3500c0156d/submodules/TelegramPresentationData/Sources/PresentationData.swift#L453
|
||||
export default function highlightningColor(rgba: [number, number, number, number?]) { |
||||
let {h, s, l} = rgbaToHsla(rgba[0], rgba[1], rgba[2]); |
||||
if(s > 0) { |
||||
s = Math.min(100, s + 5 + 0.1 * (100 - s)); |
||||
} |
||||
l = Math.max(0, l * .65); |
||||
|
||||
const hsla = `hsla(${h}, ${s}%, ${l}%, .4)`; |
||||
return hsla; |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
.color-picker { |
||||
width: 380px; |
||||
max-width: 100%; |
||||
margin: 1.1875rem auto 1rem; |
||||
user-select: none; |
||||
|
||||
&-box { |
||||
width: 100%; |
||||
height: 198px; |
||||
} |
||||
|
||||
&-box, &-color-slider, &-dragger { |
||||
overflow: visible !important; |
||||
} |
||||
|
||||
&-sliders { |
||||
margin: 1rem 0 1.125rem; |
||||
} |
||||
|
||||
&-dragger { |
||||
cursor: grab; |
||||
} |
||||
|
||||
&-inputs { |
||||
display: flex; |
||||
|
||||
.input-field { |
||||
flex: 1 1 auto; |
||||
|
||||
&:not(:first-child) { |
||||
margin-left: 1.25rem; |
||||
} |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue