Small design changes, introducing CSS colorization, doubleclick to edit markdown
This commit is contained in:
parent
43212a4487
commit
5bd5ef1f02
9 changed files with 455 additions and 100 deletions
207
static/js/lib/css_colorize.mjs
Normal file
207
static/js/lib/css_colorize.mjs
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
export class Color {
|
||||
constructor(r, g, b) { this.set(r, g, b); }
|
||||
toString() { return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(this.b)})`; }
|
||||
|
||||
set(r, g, b) {
|
||||
this.r = this.clamp(r);
|
||||
this.g = this.clamp(g);
|
||||
this.b = this.clamp(b);
|
||||
}
|
||||
|
||||
hueRotate(angle = 0) {
|
||||
angle = angle / 180 * Math.PI;
|
||||
let sin = Math.sin(angle);
|
||||
let cos = Math.cos(angle);
|
||||
|
||||
this.multiply([
|
||||
0.213 + cos * 0.787 - sin * 0.213, 0.715 - cos * 0.715 - sin * 0.715, 0.072 - cos * 0.072 + sin * 0.928,
|
||||
0.213 - cos * 0.213 + sin * 0.143, 0.715 + cos * 0.285 + sin * 0.140, 0.072 - cos * 0.072 - sin * 0.283,
|
||||
0.213 - cos * 0.213 - sin * 0.787, 0.715 - cos * 0.715 + sin * 0.715, 0.072 + cos * 0.928 + sin * 0.072
|
||||
]);
|
||||
}
|
||||
|
||||
grayscale(value = 1) {
|
||||
this.multiply([
|
||||
0.2126 + 0.7874 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 - 0.0722 * (1 - value),
|
||||
0.2126 - 0.2126 * (1 - value), 0.7152 + 0.2848 * (1 - value), 0.0722 - 0.0722 * (1 - value),
|
||||
0.2126 - 0.2126 * (1 - value), 0.7152 - 0.7152 * (1 - value), 0.0722 + 0.9278 * (1 - value)
|
||||
]);
|
||||
}
|
||||
|
||||
sepia(value = 1) {
|
||||
this.multiply([
|
||||
0.393 + 0.607 * (1 - value), 0.769 - 0.769 * (1 - value), 0.189 - 0.189 * (1 - value),
|
||||
0.349 - 0.349 * (1 - value), 0.686 + 0.314 * (1 - value), 0.168 - 0.168 * (1 - value),
|
||||
0.272 - 0.272 * (1 - value), 0.534 - 0.534 * (1 - value), 0.131 + 0.869 * (1 - value)
|
||||
]);
|
||||
}
|
||||
|
||||
saturate(value = 1) {
|
||||
this.multiply([
|
||||
0.213 + 0.787 * value, 0.715 - 0.715 * value, 0.072 - 0.072 * value,
|
||||
0.213 - 0.213 * value, 0.715 + 0.285 * value, 0.072 - 0.072 * value,
|
||||
0.213 - 0.213 * value, 0.715 - 0.715 * value, 0.072 + 0.928 * value
|
||||
]);
|
||||
}
|
||||
|
||||
multiply(matrix) {
|
||||
let newR = this.clamp(this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2]);
|
||||
let newG = this.clamp(this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5]);
|
||||
let newB = this.clamp(this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8]);
|
||||
this.r = newR; this.g = newG; this.b = newB;
|
||||
}
|
||||
|
||||
brightness(value = 1) { this.linear(value); }
|
||||
contrast(value = 1) { this.linear(value, -(0.5 * value) + 0.5); }
|
||||
|
||||
linear(slope = 1, intercept = 0) {
|
||||
this.r = this.clamp(this.r * slope + intercept * 255);
|
||||
this.g = this.clamp(this.g * slope + intercept * 255);
|
||||
this.b = this.clamp(this.b * slope + intercept * 255);
|
||||
}
|
||||
|
||||
invert(value = 1) {
|
||||
this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255);
|
||||
this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255);
|
||||
this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255);
|
||||
}
|
||||
|
||||
hsl() { // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
|
||||
let r = this.r / 255;
|
||||
let g = this.g / 255;
|
||||
let b = this.b / 255;
|
||||
let max = Math.max(r, g, b);
|
||||
let min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
let d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
|
||||
case g: h = (b - r) / d + 2; break;
|
||||
case b: h = (r - g) / d + 4; break;
|
||||
} h /= 6;
|
||||
}
|
||||
|
||||
return {
|
||||
h: h * 100,
|
||||
s: s * 100,
|
||||
l: l * 100
|
||||
};
|
||||
}
|
||||
|
||||
clamp(value) {
|
||||
if (value > 255) { value = 255; }
|
||||
else if (value < 0) { value = 0; }
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export class Solver {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
this.targetHSL = target.hsl();
|
||||
this.reusedColor = new Color(0, 0, 0); // Object pool
|
||||
}
|
||||
|
||||
solve() {
|
||||
let result = this.solveNarrow(this.solveWide());
|
||||
return {
|
||||
values: result.values,
|
||||
loss: result.loss,
|
||||
filter: this.css(result.values)
|
||||
};
|
||||
}
|
||||
|
||||
solveWide() {
|
||||
const A = 5;
|
||||
const c = 15;
|
||||
const a = [60, 180, 18000, 600, 1.2, 1.2];
|
||||
|
||||
let best = { loss: Infinity };
|
||||
for (let i = 0; best.loss > 25 && i < 3; i++) {
|
||||
let initial = [50, 20, 3750, 50, 100, 100];
|
||||
let result = this.spsa(A, a, c, initial, 1000);
|
||||
if (result.loss < best.loss) { best = result; }
|
||||
} return best;
|
||||
}
|
||||
|
||||
solveNarrow(wide) {
|
||||
const A = wide.loss;
|
||||
const c = 2;
|
||||
const A1 = A + 1;
|
||||
const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1];
|
||||
return this.spsa(A, a, c, wide.values, 500);
|
||||
}
|
||||
|
||||
spsa(A, a, c, values, iters) {
|
||||
const alpha = 1;
|
||||
const gamma = 0.16666666666666666;
|
||||
|
||||
let best = null;
|
||||
let bestLoss = Infinity;
|
||||
let deltas = new Array(6);
|
||||
let highArgs = new Array(6);
|
||||
let lowArgs = new Array(6);
|
||||
|
||||
for (let k = 0; k < iters; k++) {
|
||||
let ck = c / Math.pow(k + 1, gamma);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
deltas[i] = Math.random() > 0.5 ? 1 : -1;
|
||||
highArgs[i] = values[i] + ck * deltas[i];
|
||||
lowArgs[i] = values[i] - ck * deltas[i];
|
||||
}
|
||||
|
||||
let lossDiff = this.loss(highArgs) - this.loss(lowArgs);
|
||||
for (let i = 0; i < 6; i++) {
|
||||
let g = lossDiff / (2 * ck) * deltas[i];
|
||||
let ak = a[i] / Math.pow(A + k + 1, alpha);
|
||||
values[i] = fix(values[i] - ak * g, i);
|
||||
}
|
||||
|
||||
let loss = this.loss(values);
|
||||
if (loss < bestLoss) { best = values.slice(0); bestLoss = loss; }
|
||||
} return { values: best, loss: bestLoss };
|
||||
|
||||
function fix(value, idx) {
|
||||
let max = 100;
|
||||
if (idx === 2 /* saturate */) { max = 7500; }
|
||||
else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { max = 200; }
|
||||
|
||||
if (idx === 3 /* hue-rotate */) {
|
||||
if (value > max) { value = value % max; }
|
||||
else if (value < 0) { value = max + value % max; }
|
||||
} else if (value < 0) { value = 0; }
|
||||
else if (value > max) { value = max; }
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
loss(filters) { // Argument is array of percentages.
|
||||
let color = this.reusedColor;
|
||||
color.set(0, 0, 0);
|
||||
|
||||
color.invert(filters[0] / 100);
|
||||
color.sepia(filters[1] / 100);
|
||||
color.saturate(filters[2] / 100);
|
||||
color.hueRotate(filters[3] * 3.6);
|
||||
color.brightness(filters[4] / 100);
|
||||
color.contrast(filters[5] / 100);
|
||||
|
||||
let colorHSL = color.hsl();
|
||||
return Math.abs(color.r - this.target.r)
|
||||
+ Math.abs(color.g - this.target.g)
|
||||
+ Math.abs(color.b - this.target.b)
|
||||
+ Math.abs(colorHSL.h - this.targetHSL.h)
|
||||
+ Math.abs(colorHSL.s - this.targetHSL.s)
|
||||
+ Math.abs(colorHSL.l - this.targetHSL.l);
|
||||
}
|
||||
|
||||
css(filters) {
|
||||
function fmt(idx, multiplier = 1) { return Math.round(filters[idx] * multiplier); }
|
||||
return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(2)}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(5)}%)`;
|
||||
}
|
||||
}
|
||||
|
|
@ -110,12 +110,12 @@ export class MarkedPosition {
|
|||
renderer: {
|
||||
heading(token) {
|
||||
const content = this.parser.parseInline(token.tokens)
|
||||
return `<h${token.depth} onclick="setpos(event)" onclick="setpos(this)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n`
|
||||
return `<h${token.depth} ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</h${token.depth}>\n`
|
||||
},
|
||||
|
||||
paragraph(token) {
|
||||
const content = this.parser.parseInline(token.tokens)
|
||||
return `<p onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
|
||||
return `<p ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${content}</p>\n`
|
||||
},
|
||||
|
||||
list(token) {
|
||||
|
|
@ -134,7 +134,7 @@ export class MarkedPosition {
|
|||
},
|
||||
|
||||
listitem(token) {
|
||||
return `<li onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
|
||||
return `<li ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parse(token.tokens)}</li>\n`
|
||||
},
|
||||
|
||||
code(token) {
|
||||
|
|
@ -143,12 +143,12 @@ export class MarkedPosition {
|
|||
const code = token.text.replace(other.endingNewline, '') + '\n'
|
||||
|
||||
if (!langString) {
|
||||
return `<pre onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
|
||||
return `<pre ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code>`
|
||||
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
||||
+ '</code></pre>\n'
|
||||
}
|
||||
|
||||
return `<pre onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
|
||||
return `<pre ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"><code class="language-`
|
||||
+ escapeHtmlEntities(langString)
|
||||
+ '">'
|
||||
+ (token.escaped ? code : escapeHtmlEntities(code, true))
|
||||
|
|
@ -157,7 +157,7 @@ export class MarkedPosition {
|
|||
|
||||
blockquote(token) {
|
||||
const body = this.parser.parse(token.tokens)
|
||||
return `<blockquote onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
|
||||
return `<blockquote ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n${body}</blockquote>\n`
|
||||
},
|
||||
|
||||
html(token) {
|
||||
|
|
@ -169,11 +169,11 @@ export class MarkedPosition {
|
|||
},
|
||||
|
||||
hr(token) {
|
||||
return `<hr onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
|
||||
return `<hr ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">\n`
|
||||
},
|
||||
|
||||
checkbox(token) {
|
||||
return `<input onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
|
||||
return `<input ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}"`
|
||||
+ (token.checked ? 'checked="" ' : '')
|
||||
+ 'disabled="" type="checkbox"> '
|
||||
},
|
||||
|
|
@ -218,7 +218,7 @@ export class MarkedPosition {
|
|||
if (token.tokens.length > 0) {
|
||||
const start = token.tokens[0].position.start.offset
|
||||
const end = token.tokens[0].position.end.offset
|
||||
ofs = `onclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
|
||||
ofs = `ondblclick="setpos(event)" data-offset-start="${start}" data-offset-end="${end}"`
|
||||
}
|
||||
|
||||
const content = this.parser.parseInline(token.tokens);
|
||||
|
|
@ -230,23 +230,23 @@ export class MarkedPosition {
|
|||
},
|
||||
|
||||
strong(token) {
|
||||
return `<strong onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
|
||||
return `<strong ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</strong>`
|
||||
},
|
||||
|
||||
em(token) {
|
||||
return `<em onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
|
||||
return `<em ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</em>`
|
||||
},
|
||||
|
||||
codespan(token) {
|
||||
return `<code onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
|
||||
return `<code ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${escapeHtmlEntities(token.text, true)}</code>`
|
||||
},
|
||||
|
||||
br(token) {
|
||||
return `<br onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
|
||||
return `<br ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">`
|
||||
},
|
||||
|
||||
del(token) {
|
||||
return `<del onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
|
||||
return `<del ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}">${this.parser.parseInline(token.tokens)}</del>`
|
||||
},
|
||||
|
||||
link(token) {
|
||||
|
|
@ -256,7 +256,7 @@ export class MarkedPosition {
|
|||
return text
|
||||
}
|
||||
token.href = cleanHref
|
||||
let out = '<a onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
|
||||
let out = '<a ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" href="' + token.href + '"'
|
||||
if (token.title) {
|
||||
out += ' title="' + (escapeHtmlEntities(token.title)) + '"'
|
||||
}
|
||||
|
|
@ -275,7 +275,7 @@ export class MarkedPosition {
|
|||
}
|
||||
token.href = cleanHref
|
||||
|
||||
let out = `<img onclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
|
||||
let out = `<img ondblclick="setpos(event)" data-offset-start="${token.position.start.offset}" data-offset-end="${token.position.end.offset}" src="${token.href}" alt="${escapeHtmlEntities(token.text)}"`
|
||||
if (token.title) {
|
||||
out += ` title="${escapeHtmlEntities(token.title)}"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { ROOT_NODE } from 'node_store'
|
||||
import { CustomHTMLElement } from './lib/custom_html_element.mjs'
|
||||
import { Color, Solver } from './lib/css_colorize.mjs'
|
||||
|
||||
export class N2Tree extends CustomHTMLElement {
|
||||
static {// {{{
|
||||
this.tmpl = document.createElement('template')
|
||||
this.tmpl.innerHTML = `
|
||||
<div data-el="logo" id="logo"><img src="/images/${_VERSION}/logo.svg" /></div>
|
||||
<div class="icons">
|
||||
<div data-el="logo" id="logo">
|
||||
<img src="/images/${_VERSION}/logo_small.svg" />
|
||||
<img src="/images/${_VERSION}/logo.svg" />
|
||||
<img data-el="search" class='search' src="/images/${_VERSION}/icon_search.svg" style="height: 22px" />
|
||||
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
|
||||
</div>
|
||||
<div class="icons">
|
||||
<img data-el="sync" class='sync' src="/images/${_VERSION}/icon_refresh.svg" />
|
||||
<img data-el="settings" class='settings' src="/images/${_VERSION}/icon_settings.svg" />
|
||||
</div>
|
||||
<div data-el="treenodes"></div>
|
||||
`
|
||||
|
|
@ -31,7 +36,7 @@ export class N2Tree extends CustomHTMLElement {
|
|||
this.elSync.addEventListener('click', () => _sync.run())
|
||||
this.elLogo.addEventListener('click', () => _app.goToNode(ROOT_NODE, false, false))
|
||||
|
||||
_mbus.subscribe('NODE_MODIFIED', ({ detail })=>{
|
||||
_mbus.subscribe('NODE_MODIFIED', ({ detail }) => {
|
||||
const node = detail.data.node
|
||||
const treenode = this.treeNodeComponents[node.get('UUID')]
|
||||
|
||||
|
|
@ -43,6 +48,12 @@ export class N2Tree extends CustomHTMLElement {
|
|||
})
|
||||
|
||||
this.populateFirstLevel()
|
||||
|
||||
/* XXX - set color */
|
||||
let color = new Color(255, 96, 80)
|
||||
let solver = new Solver(color)
|
||||
let result = solver.solve()
|
||||
this.elSettings.style.filter = result.filter
|
||||
}// }}}
|
||||
render() {// {{{
|
||||
if (this.rendered)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue