From 5bd5ef1f02beda0e99992cd28ea5140ad6f1cd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Sat, 30 May 2026 09:28:36 +0200 Subject: [PATCH] Small design changes, introducing CSS colorization, doubleclick to edit markdown --- static/css/notes2.css | 64 +++++----- static/images/collapsed.svg | 22 ++-- static/images/expanded.svg | 21 ++-- static/images/icon_settings.svg | 49 ++++++++ static/images/logo.svg | 78 ++++++++---- static/images/logo_small.svg | 63 ++++++++++ static/js/lib/css_colorize.mjs | 207 ++++++++++++++++++++++++++++++++ static/js/marked_position.mjs | 32 ++--- static/js/tree.mjs | 19 ++- 9 files changed, 455 insertions(+), 100 deletions(-) create mode 100644 static/images/icon_settings.svg create mode 100644 static/images/logo_small.svg create mode 100644 static/js/lib/css_colorize.mjs diff --git a/static/css/notes2.css b/static/css/notes2.css index 7c2320f..8351858 100644 --- a/static/css/notes2.css +++ b/static/css/notes2.css @@ -52,51 +52,51 @@ html { #tree { grid-area: tree; display: grid; - background-color: #fafafa; + background-color: #ffffff; color: #444; z-index: 100; - border-right: 1px solid #ddd; - - n2-tree { - /*border: 2px solid #f8f8f8;*/ - padding: 16px 48px 16px 24px; - } - - &:focus-within { - n2-tree { - /* - border: 2px solid #fe5f55; - */ - } - - } - + border-right: 2px solid #ddd; #logo { display: grid; - position: relative; - justify-items: center; - margin-top: 8px; - margin-bottom: 8px; - margin-left: 24px; - margin-right: 24px; + grid-template-columns: min-content 1fr min-content; + align-items: center; + justify-items: start; cursor: pointer; + padding: 16px; + border-bottom: 1px solid #ccc; - img { - width: 128px; - left: -20px; + .el-search { + justify-self: end; + } + img:first-child { + height: 24px; + margin-right: 8px; } } .icons { display: flex; justify-content: center; - margin-bottom: 32px; + margin: 16px 0px 32px 0px; gap: 8px; } + n2-tree { + .el-treenodes { + margin: 32px; + } + } + + &:focus-within { + n2-tree { + } + + } + + .node { display: grid; grid-template-columns: 40px min-content; @@ -145,16 +145,6 @@ html { } } -#tree-nodes { - padding: 16px 32px; - /* - border-radius: 8px; -*/ - /* - box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75); - */ -} - #crumbs { grid-area: crumbs; display: grid; diff --git a/static/images/collapsed.svg b/static/images/collapsed.svg index d93f4ca..db06415 100644 --- a/static/images/collapsed.svg +++ b/static/images/collapsed.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="collapsed.svg" - inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" + inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="4.8373092" - inkscape:cx="6.201795" - inkscape:cy="-12.40359" - inkscape:window-width="1916" - inkscape:window-height="1161" - inkscape:window-x="0" - inkscape:window-y="18" + inkscape:zoom="19.349237" + inkscape:cx="11.809251" + inkscape:cy="6.3051583" + inkscape:window-width="1093" + inkscape:window-height="1401" + inkscape:window-x="2560" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1" showguides="false" /> @@ -42,9 +42,13 @@ transform="translate(-102.39375,-146.31458)"> folder-outline + + style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffcc00;fill-opacity:1;stroke-width:0.330728;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /> diff --git a/static/images/expanded.svg b/static/images/expanded.svg index 017e8a4..9b420a8 100644 --- a/static/images/expanded.svg +++ b/static/images/expanded.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" sodipodi:docname="expanded.svg" - inkscape:version="1.4.2 (ebf0e94, 2025-05-08)" + inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -23,13 +23,13 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="11.17754" - inkscape:cx="20.845374" - inkscape:cy="26.929003" - inkscape:window-width="1916" - inkscape:window-height="1161" - inkscape:window-x="0" - inkscape:window-y="18" + inkscape:zoom="15.807429" + inkscape:cx="10.533022" + inkscape:cy="16.384701" + inkscape:window-width="1093" + inkscape:window-height="1401" + inkscape:window-x="1463" + inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="layer1" />folder-openfolder-open-outline + style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#ffcc00;fill-opacity:1;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000000;stop-opacity:1" /> diff --git a/static/images/icon_settings.svg b/static/images/icon_settings.svg new file mode 100644 index 0000000..1b692ad --- /dev/null +++ b/static/images/icon_settings.svg @@ -0,0 +1,49 @@ + + + + + + + + cog-outline + + + diff --git a/static/images/logo.svg b/static/images/logo.svg index 3b5efa4..f294234 100644 --- a/static/images/logo.svg +++ b/static/images/logo.svg @@ -2,12 +2,12 @@ - - - - + transform="translate(-69.267449,-144.54153)"> + + + NOTES + 2 + diff --git a/static/images/logo_small.svg b/static/images/logo_small.svg new file mode 100644 index 0000000..cb83d39 --- /dev/null +++ b/static/images/logo_small.svg @@ -0,0 +1,63 @@ + + + + + + + + + N2 + + diff --git a/static/js/lib/css_colorize.mjs b/static/js/lib/css_colorize.mjs new file mode 100644 index 0000000..f0fdb37 --- /dev/null +++ b/static/js/lib/css_colorize.mjs @@ -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)}%)`; + } +} diff --git a/static/js/marked_position.mjs b/static/js/marked_position.mjs index ca85d3e..5c9c0ff 100644 --- a/static/js/marked_position.mjs +++ b/static/js/marked_position.mjs @@ -110,12 +110,12 @@ export class MarkedPosition { renderer: { heading(token) { const content = this.parser.parseInline(token.tokens) - return `${content}\n` + return `${content}\n` }, paragraph(token) { const content = this.parser.parseInline(token.tokens) - return `

${content}

\n` + return `

${content}

\n` }, list(token) { @@ -134,7 +134,7 @@ export class MarkedPosition { }, listitem(token) { - return `
  • ${this.parser.parse(token.tokens)}
  • \n` + return `
  • ${this.parser.parse(token.tokens)}
  • \n` }, code(token) { @@ -143,12 +143,12 @@ export class MarkedPosition { const code = token.text.replace(other.endingNewline, '') + '\n' if (!langString) { - return `
    `
    +						return `
    `
     							+ (token.escaped ? code : escapeHtmlEntities(code, true))
     							+ '
    \n' } - return `
    '
     						+ (token.escaped ? code : escapeHtmlEntities(code, true))
    @@ -157,7 +157,7 @@ export class MarkedPosition {
     
     				blockquote(token) {
     					const body = this.parser.parse(token.tokens)
    -					return `
    \n${body}
    \n` + return `
    \n${body}
    \n` }, html(token) { @@ -169,11 +169,11 @@ export class MarkedPosition { }, hr(token) { - return `
    \n` + return `
    \n` }, checkbox(token) { - return ` ' }, @@ -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 `${this.parser.parseInline(token.tokens)}` + return `${this.parser.parseInline(token.tokens)}` }, em(token) { - return `${this.parser.parseInline(token.tokens)}` + return `${this.parser.parseInline(token.tokens)}` }, codespan(token) { - return `${escapeHtmlEntities(token.text, true)}` + return `${escapeHtmlEntities(token.text, true)}` }, br(token) { - return `
    ` + return `
    ` }, del(token) { - return `${this.parser.parseInline(token.tokens)}` + return `${this.parser.parseInline(token.tokens)}` }, link(token) { @@ -256,7 +256,7 @@ export class MarkedPosition { return text } token.href = cleanHref - let out = '