This commit is contained in:
Liu Xiaoyi 2024-03-30 14:29:24 +08:00
parent 7b0fc276f4
commit bba165a7e6
No known key found for this signature in database
GPG Key ID: A04E02BF7E977471
14 changed files with 15690 additions and 13 deletions

View File

@ -43,6 +43,11 @@
<script src="/static/js/markup.min.js?{{ site.data['hash'] }}"></script>
<script src="/static/js/webfont.js?{{ site.data['hash'] }}"></script>
<script src="/static/js/thuhidden.js?{{ site.data['hash'] }}"></script>
<script src="/static/js/dark.js?{{ site.data['hash'] }}" type="module"></script>
<script src="/static/js/descretize-svg-path.js?{{ site.data['hash'] }}"></script>
<script src="/static/js/opentype.min.js?{{ site.data['hash'] }}"></script>
{% if page.legacy %}
<style>
.container {

View File

@ -40,17 +40,27 @@
{{ site.school }}</span>开源软件镜像站
</a>
</div>
<div class="{% unless page.legacy %}navbar-collapse collapse {% endunless %}navbar-right">
<ul class="nav navbar-nav" role="menubar">
{% unless site.hide_navbar %}
<li role="none"><a role="menuitem" href="{{ site.homelink }}">HOME</a></li>
<li role="none"><a role="menuitem" href="https://tuna.moe/events/">EVENTS</a></li>
<li role="none"><a role="menuitem" href="https://tuna.moe/blog/">BLOG</a></li>
<li role="none"><a role="menuitem" href="https://tuna.moe/feed.xml">RSS</a></li>
<li role="none"><a role="menuitem" href="https://podcast.tuna.moe/">PODCAST</a></li>
<li role="none" class="active"><a role="menuitem" aria-checked="true" href="{{ site.url }}">MIRRORS</a></li>
{% endunless %}
</ul>
</div><!--/.nav-collapse -->
<div class="nav-right">
<div class="dark-switch" data-mode="light">
<div class="dark-switch-inner" data-mode="light">
<span class="dark-switch-icon" data-active="light">{% fa_svg fas.fa-sun %}</span>
<span class="dark-switch-icon" data-active="dark">{% fa_svg fas.fa-moon %}</span>
<span class="dark-switch-icon" data-active="darker">{% fa_svg fas.fa-lightbulb %}</span>
<span class="dark-switch-icon" data-active="lighter">{% fa_svg fas.fa-fire %}</span>
</div>
</div>
<div class="{% unless page.legacy %}navbar-collapse collapse {% endunless %}navbar-right">
<ul class="nav navbar-nav" role="menubar">
{% unless site.hide_navbar %}
<li role="none"><a role="menuitem" href="{{ site.homelink }}">HOME</a></li>
<li role="none"><a role="menuitem" href="https://tuna.moe/events/">EVENTS</a></li>
<li role="none"><a role="menuitem" href="https://tuna.moe/blog/">BLOG</a></li>
<li role="none"><a role="menuitem" href="https://tuna.moe/feed.xml">RSS</a></li>
<li role="none"><a role="menuitem" href="https://podcast.tuna.moe/">PODCAST</a></li>
<li role="none" class="active"><a role="menuitem" aria-checked="true" href="{{ site.url }}">MIRRORS</a></li>
{% endunless %}
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>

1
serve.sh Normal file
View File

@ -0,0 +1 @@
docker run -it --env JEKYLL_ENV="production" -p 0.0.0.0:4000:4000 -v $(pwd):/data tunathu/mirror-web:latest bash

143
static/css/_dark.scss Normal file
View File

@ -0,0 +1,143 @@
// TODO: < 768 px width
.nav-right {
display: flex;
flex-direction: row;
align-items: center;
}
.dark-switch {
$this: &;
flex: 1;
position: relative;
height: 36px;
&-inner {
height: 36px;
width: 36px;
position: absolute;
right: 0;
top: 0;
cursor: pointer;
}
&-icon {
opacity: 0;
transform: scale(0.8);
font-size: 18px;
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
top: 0;
transition: opacity .2s ease-in;
pointer-events: none;
@keyframes icon-exit {
0% {
transform: none;
}
100% {
transform: translateY(-10px);
}
}
animation-duration: 0.2s;
animation-timing-function: ease-in;
animation-name: icon-exit;
}
@each $mode in "light", "dark", "darker", "lighter" {
&[data-mode="#{$mode}"] {
#{$this}-icon[data-active="#{$mode}"] {
transition: transform .2s ease-out, opacity .2s ease-out;
transition-delay: .15s;
opacity: .8;
transform: none;
pointer-events: all;
@keyframes icon-enter {
0% {
transform: translateY(10px);
}
100% {
transform: none;
}
}
animation-duration: 0.2s;
animation-timing-function: ease-in;
animation-name: icon-enter;
animation-delay: .1s;
&:hover {
opacity: 1;
}
}
}
}
}
.darker-surrogate {
z-index: -1;
fill: transparent;
}
.darker-text-display {
fill: currentColor;
}
.darker-canvas {
position: fixed;
top: 0;
left: 0;
height: 100vh;
width: 100vw;
z-index: 100;
pointer-events: none;
}
.label-status {
position: relative;
}
.darker-text {
position: relative;
}
.darker-rounded-debug, .darker-text-render {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.darker-text > span {
color: transparent;
}
@font-face {
font-family: 'Source Han Sans SC';
src: url('../fonts/sss-regular.otf') format('otf'),
url('../fonts/sss-regular.woff2') format('woff2');
font-style: normal;
font-weight: normal;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Source Han Sans SC';
src: url('../fonts/sss-bold.otf') format('otf'),
url('../fonts/sss-bold.woff2') format('woff2');
font-style: normal;
font-weight: bold;
text-rendering: optimizeLegibility;
}

View File

@ -21,7 +21,7 @@ $color_link_dark: #6793cf;
$color_link_hover: #62bbe7;
$color_footer_heading: #ffffff;
$color_footer_content: #bfc9d3;
$font_default: 'Lato', 'Hiragino Sans GB', 'Source Han Sans SC', 'Source Han Sans CN', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei', 'WenQuanYi Micro Hei', '微软雅黑', sans-serif;
$font_default: 'Lato', 'Source Han Sans SC', 'Hiragino Sans GB', 'Source Han Sans CN', 'Noto Sans CJK SC', 'WenQuanYi Zen Hei', 'WenQuanYi Micro Hei', '微软雅黑', sans-serif;
$color_row_hover_dark: #325475;
{% if site.issue %}
@ -70,6 +70,8 @@ $color_primary_dark: $color_thu_purple_dark;
"syntax-highlighting",
"_spinners/3-wave";
@import "dark";
{% unless site.issue %}
:root {
color-scheme: light dark;

BIN
static/fonts/sss-bold.otf Normal file

Binary file not shown.

BIN
static/fonts/sss-bold.woff2 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

537
static/js/dark.js Normal file
View File

@ -0,0 +1,537 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
console.log("喵呜喵呜喵");
var modes = ["light", "dark", "darker", "lighter"];
function loadFont(fn) {
return __awaiter(this, void 0, void 0, function () {
var url, req, resp;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
url = "/static/fonts/".concat(fn);
return [4 /*yield*/, fetch(url)];
case 1:
req = _a.sent();
return [4 /*yield*/, req.arrayBuffer()];
case 2:
resp = _a.sent();
return [2 /*return*/, opentype.parse(resp)];
}
});
});
}
var fonts = null;
// TODO: wait for fonts
var allFonts = Promise.all([
loadFont('Lato-Regular.ttf'), loadFont('sss-regular.otf'),
loadFont('Lato-Bold.ttf'), loadFont('sss-bold.otf'),
]).then(function (_a) {
var lr = _a[0], sr = _a[1], lb = _a[2], sb = _a[3];
fonts = {
regular: [lr, sr],
bold: [lb, sb],
};
});
function resolveGlyph(char, fontSize, bold) {
if (fonts === null)
throw new Error('fonts not yet loaded!');
var fontset = bold ? fonts.bold : fonts.regular;
var _loop_1 = function (font) {
var glyph = font.charToGlyph(char);
if (!glyph || glyph.name === '.notdef')
return "continue";
var path = glyph.getPath(0, 0, fontSize);
var asc = (font.ascender + font.descender) * fontSize / font.unitsPerEm;
// console.log(asc / fontSize);
// Assemble d:
var d = path.commands.map(function (e) {
if (e.type === 'M' || e.type === 'L')
return "".concat(e.type, " ").concat(e.x, " ").concat(e.y + asc);
if (e.type === 'C')
return "".concat(e.type, " ").concat(e.x1, " ").concat(e.y1 + asc, " ").concat(e.x2, " ").concat(e.y2 + asc, " ").concat(e.x, " ").concat(e.y + asc);
if (e.type === 'Q')
return "".concat(e.type, " ").concat(e.x1, " ").concat(e.y1 + asc, " ").concat(e.x, " ").concat(e.y + asc);
if (e.type === 'Z')
return 'Z';
return '';
}).join(' ');
return { value: d };
};
for (var _i = 0, fontset_1 = fontset; _i < fontset_1.length; _i++) {
var font = fontset_1[_i];
var state_1 = _loop_1(font);
if (typeof state_1 === "object")
return state_1.value;
}
console.warn("Glyph not found: ".concat(char));
return null;
}
var discretizationCache = new Map();
function discretize(path) {
if (discretizationCache.has(path))
return discretizationCache.get(path);
var loop = window.discretizeSvgPath(path).map(function (_a) {
var x = _a[0], y = _a[1];
return ({ x: x, y: y });
});
var dx = loop[0].x - loop[loop.length - 1].x;
var dy = loop[0].y - loop[loop.length - 1].y;
if (dx < 0.001 && dy < 0.001)
loop.pop();
// for(let i = 0; i < cnt; ++i) {
// const t = (i + 1) * stepLen;
// const pt = el.getPointAtLength(t);
// const last = loop[loop.length - 1];
// const dx = last.x - pt.x;
// const dy = last.y - pt.y;
// if(dx * dx + dy * dy > stepLen * stepLen * 1.05) {
// // TODO: assert this is in current loop
// throw new Error(`Unexpected multiple-region loops: ${t} ${el.getAttribute('d')}`)
// }
// loop.push({ x: pt.x, y: pt.y });
// }
// Invert clockwise stuff
var totalAng = 0;
var angLog = [];
for (var i = 0; i < loop.length; ++i) {
var cur = loop[i];
var prev = loop[(i - 1 + loop.length) % loop.length];
var next = loop[(i + 1) % loop.length];
var dx1 = cur.x - prev.x;
var dy1 = cur.y - prev.y;
var dx2 = next.x - cur.x;
var dy2 = next.y - cur.y;
var ang = Math.atan2(dy2, dx2) - Math.atan2(dy1, dx1);
if (ang > Math.PI)
ang -= Math.PI * 2;
if (ang < -Math.PI)
ang += Math.PI * 2;
totalAng += ang;
angLog.push(ang);
}
var rot = totalAng / (Math.PI * 2);
if (Math.abs(rot) > 1.01 || Math.abs(rot) < 0.99)
console.error(rot, loop, angLog);
if (rot < 0) { // However remember we're up-side down. This turns it into clockwise (in our view)
loop.reverse();
}
discretizationCache.set(path, loop);
return loop;
}
function applyMode(m) {
if (m === 'darker') {
rescanAt(document.body);
ensureDarker();
renderLoop();
}
}
var mx = 0;
var my = 0;
function tracker(e) {
mx = e.clientX;
my = e.clientY;
}
function rescan(mutations, obs) {
for (var _i = 0, mutations_1 = mutations; _i < mutations_1.length; _i++) {
var m = mutations_1[_i];
if (m.type === 'attributes')
continue;
if (m.type === 'characterData') {
// TODO: rescan node
console.log('Don\'t know how to rescan characterData');
continue;
}
for (var _a = 0, _b = m.addedNodes; _a < _b.length; _a++) {
var n = _b[_a];
rescanAt(n);
}
}
}
// TODO: allow scaning arbitrary HTML-side nodes
function rescanAt(el) {
var _a;
// Check if is svg
if (el.tagName === 'svg') {
rescanSVG(el);
return;
}
if ((_a = el.classList) === null || _a === void 0 ? void 0 : _a.contains('label-status')) {
var r = parseFloat(window.getComputedStyle(el).borderRadius.match(/^[0-9.]+/)[0]);
var _b = el.getBoundingClientRect(), width = _b.width, height = _b.height;
var d = "\n M 0 ".concat(r, " A ").concat(r, " ").concat(r, " 0 0 1 ").concat(r, " 0\n L ").concat(width - r, " 0 A ").concat(r, " ").concat(r, " 0 0 1 ").concat(width, " ").concat(r, "\n L ").concat(width, " ").concat(height - r, " A ").concat(r, " ").concat(r, " 0 0 1 ").concat(width - r, " ").concat(height, "\n L ").concat(r, " ").concat(height, " A ").concat(r, " ").concat(r, " 0 0 1 0 ").concat(height - r, "\n z\n ");
var svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
svg.setAttribute('viewbox', "0 0 ".concat(width, " ").concat(height));
svg.style.width = width + 'px';
svg.style.height = height + 'px';
svg.classList.add('darker-rounded-debug');
var path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', d);
path.classList.add('darker-processed');
path.classList.add('darker-surrogate');
svg.appendChild(path);
el.appendChild(svg);
svg.classList.add('darker-traced');
rescanAt(svg);
}
for (var _i = 0, _c = el.childNodes; _i < _c.length; _i++) {
var child = _c[_i];
if (child.nodeType === Node.TEXT_NODE) {
var inner = child.nodeValue;
if (!inner)
continue;
inner = inner.replace('\n', ' ');
var wrapper = document.createElement('span');
wrapper.classList.add('darker-text-group');
var styles = window.getComputedStyle(el);
var fs = styles.fontSize;
var fsNum = parseFloat(fs.match(/^[0-9.]+/)[0]);
var isBold = styles.fontWeight !== '400';
var _loop_2 = function () {
// Trim empty stuff
var startTrim = inner.length - inner.trimStart().length;
if (startTrim != 0) {
var startEmpty = inner.substring(0, startTrim);
var node = document.createElement('span');
node.classList.add('darker-text-empty');
node.innerText = ' ';
wrapper.appendChild(node);
inner = inner.trimStart();
}
else {
var first = inner.substring(0, 1);
inner = inner.substring(1);
var node_1 = document.createElement('span');
node_1.classList.add('darker-text');
var holder = document.createElement('span');
holder.innerText = first;
node_1.appendChild(holder);
wrapper.appendChild(node_1);
// console.log(fsNum);
var glyph_1 = resolveGlyph(first, fsNum, isBold);
if (!glyph_1)
return "continue";
node_1.classList.add('darker-traced');
node_1.setAttribute('data-glyph', glyph_1);
// console.log(first, glyph);
// Debug
// TODO: drop me
setTimeout(function () {
var _a = node_1.getBoundingClientRect(), width = _a.width, height = _a.height;
var svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
svg.setAttribute('viewbox', "0 0 ".concat(width, " ").concat(height));
svg.style.width = width + 'px';
svg.style.height = height + 'px';
svg.classList.add('darker-text-render');
var path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', glyph_1);
path.classList.add('darker-processed');
path.classList.add('darker-text-display');
svg.appendChild(path);
node_1.appendChild(svg);
});
}
};
while (inner !== '') {
_loop_2();
}
el.replaceChild(wrapper, child);
}
else {
// Skip processed elements
if (el.classList.contains('darker-text-group'))
continue;
if (el.classList.contains('darker-text-svg'))
continue;
rescanAt(child);
}
}
}
var symbolCache = {};
var svgCache = {};
var svgIDGen = 0;
function splitPathSegs(path) {
var d = path;
var segs = [];
while (true) {
// console.log(d);
var nextMoveIdx = d.substring(1).toLowerCase().indexOf('m');
if (nextMoveIdx === -1) {
segs.push(d);
break;
}
segs.push(d.substring(0, nextMoveIdx + 1));
d = d.substring(nextMoveIdx + 1);
}
var last = { x: 0, y: 0 };
var paths = [];
for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) {
var seg = segs_1[_i];
var _a = seg.match(/^[mM] *(-?[.0-9]+) *(-?[.0-9]+)/), firstMove = _a[0], mx_1 = _a[1], my_1 = _a[2];
// console.log(firstMove);
var d_1 = firstMove[0] === 'M' ? seg : "M ".concat(last.x + parseFloat(mx_1), " ").concat(last.y + parseFloat(my_1), " ").concat(seg.substring(firstMove.length));
var path_1 = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path_1.setAttribute('d', d_1);
path_1.classList.add('darker-processed');
path_1.classList.add('darker-surrogate');
last = path_1.getPointAtLength(path_1.getTotalLength());
paths.push([path_1, new Path2D(d_1)]);
}
var tmpCtx = document.createElement('canvas').getContext('2d');
var outerPaths = [];
for (var _b = 0, paths_1 = paths; _b < paths_1.length; _b++) {
var _c = paths_1[_b], path_2 = _c[0], _ = _c[1];
var starting = path_2.getPointAtLength(0);
var outer = true;
for (var _d = 0, paths_2 = paths; _d < paths_2.length; _d++) {
var _e = paths_2[_d], another = _e[0], repr = _e[1];
if (another !== path_2) {
if (tmpCtx.isPointInPath(repr, starting.x, starting.y)) {
outer = false;
break;
}
}
}
if (outer)
outerPaths.push(path_2);
}
return outerPaths;
}
// TODO: cache DOM
function rescanSVG(el) {
var _a;
if (el.classList.contains('darker-processed'))
return;
if (el.tagName === 'path') {
var d = (_a = el.getAttribute('d')) !== null && _a !== void 0 ? _a : '';
var segs = splitPathSegs(d);
for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) {
var path = segs_2[_i];
el.parentNode.appendChild(path);
}
}
else if (el.tagName === 'use') {
el.classList.add('darker-traced');
// const xlink = el.getAttribute('xlink:href');
}
// TODO: trace texts
for (var _b = 0, _c = el.children; _b < _c.length; _b++) {
var child = _c[_b];
rescanSVG(child);
}
if (el.tagName === 'symbol' && el.id !== '') {
// Cache symbol content
var allPaths = el.querySelectorAll(".darker-surrogate");
var ret = [];
for (var _d = 0, allPaths_1 = allPaths; _d < allPaths_1.length; _d++) {
var p = allPaths_1[_d];
ret.push(p.getAttribute('d'));
}
symbolCache[el.id] = ret;
}
else if (el.tagName === 'svg' && el.getAttribute('display') !== 'none') {
var id = svgIDGen++;
el.id = "darker-svg-".concat(id);
var allPaths = el.querySelectorAll(".darker-surrogate");
var ret = [];
for (var _e = 0, allPaths_2 = allPaths; _e < allPaths_2.length; _e++) {
var p = allPaths_2[_e];
ret.push(p.getAttribute('d'));
}
svgCache[el.id] = ret;
}
el.classList.add('darker-processed');
}
var obs = null;
var canvas = null;
function ensureDarker() {
if (obs === null) {
obs = new MutationObserver(rescan);
obs.observe(document.body, {
childList: true,
subtree: true,
});
}
if (canvas === null) {
canvas = document.createElement('canvas');
canvas.classList.add('darker-canvas');
document.body.appendChild(canvas);
}
}
var textCache = new WeakMap();
var renderStopped = false;
function renderLoop() {
if (renderStopped)
return;
if (canvas === null)
return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext('2d');
var traced = document.getElementsByClassName('darker-traced');
var cnt = 0;
function tracePaths(paths, sx, sy, width, height, scale) {
var _loop_3 = function (path) {
var dpath = discretize(path);
if (dpath.length === 1)
return "continue";
ctx.beginPath();
var lastAng = null;
var incRegionStart = null;
function commit(x, y) {
if (incRegionStart === null)
return;
var color = "black";
// if(x < sx || x > sx + width || y < sy || y > sy + height) {
// color = 'red';
// }
var bx = incRegionStart.x, by = incRegionStart.y;
// if(bx < sx || bx > sx + width || by < sy || by > sy + height) {
// color = 'green';
// }
ctx.fillStyle = "".concat(color);
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo((bx - mx) * 1000 + bx, (by - my) * 1000 + by);
ctx.lineTo((x - mx) * 1000 + x, (y - my) * 1000 + y);
ctx.lineTo(x, y);
ctx.fill();
cnt += 2;
lastAng = null;
incRegionStart = null;
}
for (var i = 0; i < dpath.length; ++i) {
var cx = dpath[i].x * scale + sx;
var cy = dpath[i].y * scale + sy;
var nx = dpath[(i + 1) % dpath.length].x * scale + sx;
var ny = dpath[(i + 1) % dpath.length].y * scale + sy;
// Test angle. This is a counter-clockwise loop (in canonical axis orientation)
var segAng = Math.atan2(nx - cx, ny - cy);
var rayAng = Math.atan2(cx - mx, cy - my);
var diffAng = segAng - rayAng;
if (diffAng > Math.PI)
diffAng -= Math.PI * 2;
if (diffAng < -Math.PI)
diffAng += Math.PI * 2;
if (diffAng < 0 || diffAng > Math.PI) {
commit(cx, cy);
// TODO: commit
continue;
}
// Check for continous increasing region
if (lastAng === null) {
lastAng = segAng;
incRegionStart = { x: cx, y: cy };
continue;
}
var diffLastAng = segAng - rayAng;
if (diffLastAng > Math.PI)
diffLastAng -= Math.PI * 2;
if (diffLastAng < -Math.PI)
diffLastAng += Math.PI * 2;
if (diffLastAng > 0) {
lastAng = segAng;
continue;
}
else {
commit(cx, cy);
lastAng = segAng;
continue;
}
}
var bx = dpath[0].x * scale + sx;
var by = dpath[0].y * scale + sy;
commit(bx, by);
};
for (var _i = 0, paths_3 = paths; _i < paths_3.length; _i++) {
var path = paths_3[_i];
_loop_3(path);
}
}
for (var _i = 0, traced_1 = traced; _i < traced_1.length; _i++) {
var trace = traced_1[_i];
var _a = trace.getBoundingClientRect(), sx = _a.x, sy = _a.y, width = _a.width, height = _a.height;
var scale = 1;
if (trace.tagName === 'use') {
var sym = document.getElementById(trace.getAttribute('xlink:href').substring(1));
var vbox = sym.viewBox.baseVal;
// TODO: handle browsers without baseVal
// TODO: handle origins other than 0,0
var scale_1 = width / vbox.width;
var vscale = height / vbox.height;
// if(scale > vscale * 1.01 || scale < vscale * 0.99)
// console.warn(`incompatible scales: ${scale}, ${vscale}`);
var paths = symbolCache[sym.id];
if (paths === undefined) {
console.warn("Symbol not in cache: ".concat(sym.id));
continue;
}
tracePaths(paths, sx, sy, width, height, scale_1);
}
else if (trace.tagName === 'svg') {
var paths = svgCache[trace.id];
if (paths === undefined) {
console.warn("SVG not in cache: ".concat(trace.id));
continue;
}
tracePaths(paths, sx, sy, width, height, 1);
}
else if (trace.classList.contains('darker-text')) {
var cached = textCache.get(trace);
if (!cached) {
var glyph = trace.getAttribute('data-glyph');
var paths = splitPathSegs(glyph);
cached = paths.map(function (e) { return e.getAttribute('d'); });
textCache.set(trace, cached);
}
tracePaths(cached, sx, sy, width, height, 1);
}
}
console.log(cnt);
requestAnimationFrame(renderLoop);
}
document.addEventListener('DOMContentLoaded', function () {
var dark = document.getElementsByClassName('dark-switch-inner')[0];
dark.addEventListener('click', function () {
var parent = dark.parentNode;
var cur = parent.getAttribute("data-mode");
var idx = (modes.findIndex(function (e) { return e === cur; }) + 1) % modes.length;
var next = modes[idx];
applyMode(next);
parent.setAttribute('data-mode', next);
});
document.addEventListener('mousemove', tracker);
});

514
static/js/dark.ts Normal file
View File

@ -0,0 +1,514 @@
console.log("喵呜喵呜喵");
const modes = ["light", "dark", "darker", "lighter"] as const;
type Mode = typeof modes[number];
type DiscreteLoop = { x: number, y: number }[];
async function loadFont(fn: string): Promise<any> {
const url = `/static/fonts/${fn}`;
const req = await fetch(url);
const resp = await req.arrayBuffer();
return opentype.parse(resp);
}
let fonts: null | { regular: any[], bold: any[] } = null;
// TODO: wait for fonts
const allFonts = Promise.all([
loadFont('Lato-Regular.ttf'), loadFont('sss-regular.otf'),
loadFont('Lato-Bold.ttf'), loadFont('sss-bold.otf'),
]).then(([lr, sr, lb, sb]) => {
fonts = {
regular: [lr, sr],
bold: [lb, sb],
}
})
function resolveGlyph(char: string, fontSize: number, bold: boolean): string | null {
if(fonts === null) throw new Error('fonts not yet loaded!');
const fontset = bold ? fonts.bold : fonts.regular;
for(const font of fontset) {
const glyph = font.charToGlyph(char);
if(!glyph || glyph.name === '.notdef') continue;
const path = glyph.getPath(0, 0, fontSize);
const asc = (font.ascender + font.descender) * fontSize / font.unitsPerEm;
// console.log(asc / fontSize);
// Assemble d:
const d = path.commands.map(e => {
if(e.type === 'M' || e.type === 'L') return `${e.type} ${e.x} ${e.y + asc}`;
if(e.type === 'C') return `${e.type} ${e.x1} ${e.y1 + asc} ${e.x2} ${e.y2 + asc} ${e.x} ${e.y + asc}`;
if(e.type === 'Q') return `${e.type} ${e.x1} ${e.y1 + asc} ${e.x} ${e.y + asc}`;
if(e.type === 'Z') return 'Z';
return '';
}).join(' ');
return d;
}
console.warn(`Glyph not found: ${char}`);
return null;
}
const discretizationCache = new Map();
function discretize(path: string): DiscreteLoop {
if(discretizationCache.has(path)) return discretizationCache.get(path);
let loop: DiscreteLoop = window.discretizeSvgPath(path).map(([x, y]) => ({x, y}));
const dx = loop[0].x - loop[loop.length - 1].x;
const dy = loop[0].y - loop[loop.length - 1].y;
if(dx < 0.001 && dy < 0.001) loop.pop();
// for(let i = 0; i < cnt; ++i) {
// const t = (i + 1) * stepLen;
// const pt = el.getPointAtLength(t);
// const last = loop[loop.length - 1];
// const dx = last.x - pt.x;
// const dy = last.y - pt.y;
// if(dx * dx + dy * dy > stepLen * stepLen * 1.05) {
// // TODO: assert this is in current loop
// throw new Error(`Unexpected multiple-region loops: ${t} ${el.getAttribute('d')}`)
// }
// loop.push({ x: pt.x, y: pt.y });
// }
// Invert clockwise stuff
let totalAng = 0;
const angLog: number[] = []
for(let i = 0; i < loop.length; ++i) {
const cur = loop[i];
const prev = loop[(i - 1 + loop.length) % loop.length];
const next = loop[(i + 1) % loop.length];
const dx1 = cur.x - prev.x;
const dy1 = cur.y - prev.y;
const dx2 = next.x - cur.x;
const dy2 = next.y - cur.y;
let ang = Math.atan2(dy2, dx2) - Math.atan2(dy1, dx1);
if(ang > Math.PI) ang -= Math.PI * 2;
if(ang < -Math.PI) ang += Math.PI * 2;
totalAng += ang;
angLog.push(ang);
}
const rot = totalAng / (Math.PI * 2);
if(Math.abs(rot) > 1.01 || Math.abs(rot) < 0.99)
console.error(rot, loop, angLog);
if(rot < 0) { // However remember we're up-side down. This turns it into clockwise (in our view)
loop.reverse();
}
discretizationCache.set(path, loop);
return loop;
}
function applyMode(m: Mode) {
if(m === 'darker') {
rescanAt(document.body);
ensureDarker();
renderLoop();
}
}
let mx = 0;
let my = 0;
function tracker(e: MouseEvent) {
mx = e.clientX;
my = e.clientY;
}
function rescan(mutations: MutationRecord[], obs: MutationObserver) {
for(const m of mutations) {
if(m.type === 'attributes') continue;
if(m.type === 'characterData') {
// TODO: rescan node
console.log('Don\'t know how to rescan characterData');
continue;
}
for(const n of m.addedNodes) rescanAt(n as HTMLElement);
}
}
// TODO: allow scaning arbitrary HTML-side nodes
function rescanAt(el: HTMLElement) {
// Check if is svg
if(el.tagName === 'svg') {
rescanSVG(el as unknown as SVGElement);
return;
}
if(el.classList?.contains('label-status')) {
const r = parseFloat(window.getComputedStyle(el).borderRadius.match(/^[0-9.]+/)![0]);
const { width, height } = el.getBoundingClientRect();
const d = `
M 0 ${r} A ${r} ${r} 0 0 1 ${r} 0
L ${ width - r } 0 A ${r} ${r} 0 0 1 ${width} ${r}
L ${width} ${ height - r } A ${r} ${r} 0 0 1 ${width - r} ${height}
L ${r} ${ height } A ${r} ${r} 0 0 1 0 ${height - r}
z
`;
const svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
svg.setAttribute('viewbox', `0 0 ${width} ${height}`);
svg.style.width = width + 'px';
svg.style.height = height + 'px';
svg.classList.add('darker-rounded-debug');
const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', d);
path.classList.add('darker-processed');
path.classList.add('darker-surrogate');
svg.appendChild(path);
el.appendChild(svg);
svg.classList.add('darker-traced');
rescanAt(svg as unknown as HTMLElement);
}
for(const child of el.childNodes) {
if(child.nodeType === Node.TEXT_NODE) {
let inner = child.nodeValue;
if(!inner) continue;
inner = inner.replace('\n', ' ');
const wrapper = document.createElement('span');
wrapper.classList.add('darker-text-group');
const styles = window.getComputedStyle(el);
const fs = styles.fontSize;
const fsNum = parseFloat(fs.match(/^[0-9.]+/)![0]);
const isBold = styles.fontWeight !== '400';
while(inner !== '') {
// Trim empty stuff
const startTrim = inner.length - inner.trimStart().length;
if(startTrim != 0) {
const startEmpty = inner.substring(0, startTrim);
const node = document.createElement('span');
node.classList.add('darker-text-empty');
node.innerText = ' ';
wrapper.appendChild(node);
inner = inner.trimStart();
} else {
const first = inner.substring(0, 1);
inner = inner.substring(1);
const node = document.createElement('span');
node.classList.add('darker-text');
const holder = document.createElement('span');
holder.innerText = first;
node.appendChild(holder);
wrapper.appendChild(node);
// console.log(fsNum);
const glyph = resolveGlyph(first, fsNum, isBold);
if(!glyph) continue;
node.classList.add('darker-traced');
node.setAttribute('data-glyph', glyph);
// console.log(first, glyph);
// Debug
// TODO: drop me
setTimeout(() => {
const { width, height } = node.getBoundingClientRect();
const svg = document.createElementNS("http://www.w3.org/2000/svg", 'svg');
svg.setAttribute('viewbox', `0 0 ${width} ${height}`);
svg.style.width = width + 'px';
svg.style.height = height + 'px';
svg.classList.add('darker-text-render');
const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', glyph);
path.classList.add('darker-processed');
path.classList.add('darker-text-display');
svg.appendChild(path);
node.appendChild(svg);
});
}
}
el.replaceChild(wrapper, child);
} else {
// Skip processed elements
if(el.classList.contains('darker-text-group')) continue;
if(el.classList.contains('darker-text-svg')) continue;
rescanAt(child as HTMLElement);
}
}
}
const symbolCache: Record<string, string[]> = {};
const svgCache: Record<string, string[]> = {};
let svgIDGen = 0;
function splitPathSegs(path: string): SVGPathElement[] {
let d = path;
const segs: string[] = [];
while(true) {
// console.log(d);
const nextMoveIdx = d.substring(1).toLowerCase().indexOf('m');
if(nextMoveIdx === -1) {
segs.push(d);
break;
}
segs.push(d.substring(0, nextMoveIdx + 1));
d = d.substring(nextMoveIdx + 1);
}
let last = { x: 0, y: 0 };
const paths: [SVGPathElement, Path2D][] = [];
for(const seg of segs) {
const [firstMove, mx, my] = seg.match(/^[mM] *(-?[.0-9]+) *(-?[.0-9]+)/)!;
// console.log(firstMove);
const d = firstMove[0] === 'M' ? seg : `M ${last.x + parseFloat(mx)} ${last.y + parseFloat(my)} ${seg.substring(firstMove.length)}`;
const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', d);
path.classList.add('darker-processed');
path.classList.add('darker-surrogate');
last = path.getPointAtLength(path.getTotalLength());
paths.push([path, new Path2D(d)]);
}
const tmpCtx = document.createElement('canvas').getContext('2d')!;
const outerPaths: SVGPathElement[] = [];
for(const [path, _] of paths) {
const starting = path.getPointAtLength(0);
let outer = true;
for(const [another, repr] of paths) if(another !== path) {
if(tmpCtx.isPointInPath(repr, starting.x, starting.y)) {
outer = false;
break;
}
}
if(outer) outerPaths.push(path);
}
return outerPaths;
}
// TODO: cache DOM
function rescanSVG(el: SVGElement) {
if(el.classList.contains('darker-processed')) return;
if(el.tagName === 'path') {
let d = el.getAttribute('d') ?? '';
const segs = splitPathSegs(d);
for(const path of segs) el.parentNode!.appendChild(path);
} else if(el.tagName === 'use') {
el.classList.add('darker-traced');
// const xlink = el.getAttribute('xlink:href');
}
// TODO: trace texts
for(const child of el.children) {
rescanSVG(child as SVGElement);
}
if(el.tagName === 'symbol' && el.id !== '') {
// Cache symbol content
const allPaths = el.querySelectorAll(".darker-surrogate");
const ret: string[] = [];
for(const p of allPaths) ret.push(p.getAttribute('d')!);
symbolCache[el.id] = ret;
} else if(el.tagName === 'svg' && el.getAttribute('display') !== 'none') {
const id = svgIDGen++;
el.id = `darker-svg-${id}`;
const allPaths = el.querySelectorAll(".darker-surrogate");
const ret: string[] = [];
for(const p of allPaths) ret.push(p.getAttribute('d')!);
svgCache[el.id] = ret;
}
el.classList.add('darker-processed');
}
let obs: MutationObserver | null = null;
let canvas: HTMLCanvasElement | null = null;
function ensureDarker() {
if(obs === null) {
obs = new MutationObserver(rescan)
obs.observe(document.body, {
childList: true,
subtree: true,
});
}
if(canvas === null) {
canvas = document.createElement('canvas');
canvas.classList.add('darker-canvas');
document.body.appendChild(canvas);
}
}
const textCache: WeakMap<Element, string[]> = new WeakMap();
let renderStopped = false;
function renderLoop() {
if(renderStopped) return;
if(canvas === null) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d')!;
const traced = document.getElementsByClassName('darker-traced');
let cnt = 0;
function tracePaths(paths: string[], sx: number, sy: number, width: number, height: number, scale: number) {
for(const path of paths) {
const dpath = discretize(path);
if(dpath.length === 1) continue;
ctx.beginPath();
let lastAng: number | null = null;
let incRegionStart: { x: number, y: number } | null = null;
function commit(x: number, y: number) {
if(incRegionStart === null) return;
let color = `black`;
// if(x < sx || x > sx + width || y < sy || y > sy + height) {
// color = 'red';
// }
const { x: bx, y: by } = incRegionStart;
// if(bx < sx || bx > sx + width || by < sy || by > sy + height) {
// color = 'green';
// }
ctx.fillStyle = `${color}`;
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo((bx - mx) * 1000 + bx, (by - my) * 1000 + by);
ctx.lineTo((x - mx) * 1000 + x, (y - my) * 1000 + y);
ctx.lineTo(x, y);
ctx.fill();
cnt += 2;
lastAng = null;
incRegionStart = null;
}
for(let i = 0; i < dpath.length; ++i) {
const cx = dpath[i].x * scale + sx;
const cy = dpath[i].y * scale + sy;
const nx = dpath[(i + 1) % dpath.length].x * scale + sx;
const ny = dpath[(i + 1) % dpath.length].y * scale + sy;
// Test angle. This is a counter-clockwise loop (in canonical axis orientation)
const segAng = Math.atan2(nx - cx, ny - cy);
const rayAng = Math.atan2(cx - mx, cy - my);
let diffAng = segAng - rayAng;
if(diffAng > Math.PI) diffAng -= Math.PI * 2;
if(diffAng < -Math.PI) diffAng += Math.PI * 2;
if(diffAng < 0 || diffAng > Math.PI) {
commit(cx, cy);
// TODO: commit
continue;
}
// Check for continous increasing region
if(lastAng === null) {
lastAng = segAng;
incRegionStart = { x: cx, y: cy };
continue;
}
let diffLastAng = segAng - rayAng;
if(diffLastAng > Math.PI) diffLastAng -= Math.PI * 2;
if(diffLastAng < -Math.PI) diffLastAng += Math.PI * 2;
if(diffLastAng > 0) {
lastAng = segAng;
continue;
} else {
commit(cx, cy);
lastAng = segAng;
continue;
}
}
const bx = dpath[0].x * scale + sx;
const by = dpath[0].y * scale + sy;
commit(bx, by);
}
}
for(const trace of traced) {
const { x: sx, y: sy, width, height } = trace.getBoundingClientRect();
let scale = 1;
if(trace.tagName === 'use') {
const sym = document.getElementById(trace.getAttribute('xlink:href')!.substring(1)) as unknown as SVGSymbolElement;
const vbox = sym.viewBox.baseVal;
// TODO: handle browsers without baseVal
// TODO: handle origins other than 0,0
const scale = width / vbox.width;
const vscale = height / vbox.height;
// if(scale > vscale * 1.01 || scale < vscale * 0.99)
// console.warn(`incompatible scales: ${scale}, ${vscale}`);
const paths: string[] | undefined = symbolCache[sym.id];
if(paths === undefined) {
console.warn(`Symbol not in cache: ${sym.id}`);
continue;
}
tracePaths(paths, sx, sy, width, height, scale)
} else if(trace.tagName === 'svg') {
const paths: string[] | undefined = svgCache[trace.id];
if(paths === undefined) {
console.warn(`SVG not in cache: ${trace.id}`);
continue;
}
tracePaths(paths, sx, sy, width, height, 1);
} else if(trace.classList.contains('darker-text')) {
let cached = textCache.get(trace);
if(!cached) {
const glyph = trace.getAttribute('data-glyph')!;
const paths = splitPathSegs(glyph);
cached = paths.map(e => e.getAttribute('d')!);
textCache.set(trace, cached);
}
tracePaths(cached, sx, sy, width, height, 1);
}
}
console.log(cnt);
requestAnimationFrame(renderLoop);
}
document.addEventListener('DOMContentLoaded', () => {
const dark = document.getElementsByClassName('dark-switch-inner')[0];
dark.addEventListener('click', () => {
const parent = dark.parentNode as HTMLElement;
const cur = parent.getAttribute("data-mode");
const idx = (modes.findIndex(e => e === cur) + 1) % modes.length;
const next = modes[idx];
applyMode(next);
parent.setAttribute('data-mode', next);
})
document.addEventListener('mousemove', tracker);
})

File diff suppressed because one or more lines are too long

4
static/js/opentype.min.js vendored Normal file

File diff suppressed because one or more lines are too long

14460
static/js/opentype.module.js Normal file

File diff suppressed because it is too large Load Diff