feat: claims for having the js parts in (not working, though)
This commit is contained in:
299
priv/preview_assets/assets/calendar-runtime.js
Normal file
299
priv/preview_assets/assets/calendar-runtime.js
Normal file
@@ -0,0 +1,299 @@
|
||||
(() => {
|
||||
const button = document.querySelector('[data-blog-calendar-toggle]');
|
||||
const panel = document.querySelector('[data-blog-calendar-panel]');
|
||||
const closeButton = document.querySelector('[data-blog-calendar-close]');
|
||||
const calendarRoot = document.querySelector('[data-blog-calendar-root]');
|
||||
const status = document.querySelector('[data-blog-calendar-status]');
|
||||
|
||||
if (!button || !panel || !calendarRoot || !status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const languagePrefix = document.documentElement.getAttribute('data-language-prefix') || '';
|
||||
|
||||
const labels = {
|
||||
loading: panel.getAttribute('data-i18n-loading') || 'Loading calendar…',
|
||||
error: panel.getAttribute('data-i18n-error') || 'Calendar data could not be loaded.',
|
||||
};
|
||||
|
||||
let isInitialized = false;
|
||||
let years = {};
|
||||
let months = {};
|
||||
let days = {};
|
||||
let maxYearCount = 0;
|
||||
let maxMonthCount = 0;
|
||||
let maxDayCount = 0;
|
||||
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function normalizeCountMap(value) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
const map = {};
|
||||
for (const [key, rawCount] of Object.entries(value)) {
|
||||
const count = Number(rawCount);
|
||||
if (!Number.isFinite(count) || count <= 0) {
|
||||
continue;
|
||||
}
|
||||
map[key] = Math.floor(count);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function computeMaxCount(value) {
|
||||
const counts = Object.values(value || {});
|
||||
if (counts.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(...counts.map((count) => Number(count) || 0));
|
||||
}
|
||||
|
||||
function applyHeatStyle(target, count, maxCount) {
|
||||
if (!(target instanceof HTMLElement) || !Number.isFinite(count) || count <= 0 || !Number.isFinite(maxCount) || maxCount <= 0) {
|
||||
target?.style?.setProperty('--blog-calendar-heat-alpha', '0');
|
||||
target?.style?.setProperty('--blog-calendar-heat-hue', '210');
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = Math.min(1, count / maxCount);
|
||||
const hue = Math.round(210 - (210 * normalized));
|
||||
const alpha = (0.30 + normalized * 0.65).toFixed(3);
|
||||
|
||||
target.style.setProperty('--blog-calendar-heat-hue', String(hue));
|
||||
target.style.setProperty('--blog-calendar-heat-alpha', alpha);
|
||||
}
|
||||
|
||||
function navigateTo(pathname) {
|
||||
if (!pathname) {
|
||||
return;
|
||||
}
|
||||
window.location.assign(languagePrefix + pathname);
|
||||
}
|
||||
|
||||
function parseInitialYearMonth() {
|
||||
const initialYearRaw = button.getAttribute('data-blog-calendar-year');
|
||||
const initialMonthRaw = button.getAttribute('data-blog-calendar-month');
|
||||
|
||||
const initialYear = Number(initialYearRaw);
|
||||
const initialMonth = Number(initialMonthRaw);
|
||||
|
||||
let selectedYear = Number.isInteger(initialYear) && initialYear > 0 ? initialYear : null;
|
||||
let selectedMonth = Number.isInteger(initialMonth) && initialMonth >= 1 && initialMonth <= 12
|
||||
? (initialMonth - 1)
|
||||
: null;
|
||||
|
||||
if (!Number.isInteger(selectedYear) || !Number.isInteger(selectedMonth)) {
|
||||
const rawPathname = window.location.pathname || '';
|
||||
const pathname = languagePrefix && rawPathname.startsWith(languagePrefix + '/')
|
||||
? rawPathname.slice(languagePrefix.length)
|
||||
: rawPathname;
|
||||
const parts = pathname.split('/').filter(Boolean);
|
||||
const pathYear = Number(parts[0]);
|
||||
const pathMonth = Number(parts[1]);
|
||||
|
||||
if (!Number.isInteger(selectedYear) && Number.isInteger(pathYear) && pathYear > 0 && String(parts[0]).length === 4) {
|
||||
selectedYear = pathYear;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(selectedMonth) && Number.isInteger(pathMonth) && pathMonth >= 1 && pathMonth <= 12) {
|
||||
selectedMonth = pathMonth - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { selectedYear, selectedMonth };
|
||||
}
|
||||
|
||||
async function loadCalendarData() {
|
||||
const response = await fetch('/calendar.json', { cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
throw new Error('calendar.json request failed');
|
||||
}
|
||||
|
||||
const parsed = await response.json();
|
||||
years = normalizeCountMap(parsed?.years);
|
||||
months = normalizeCountMap(parsed?.months);
|
||||
days = normalizeCountMap(parsed?.days);
|
||||
maxYearCount = computeMaxCount(years);
|
||||
maxMonthCount = computeMaxCount(months);
|
||||
maxDayCount = computeMaxCount(days);
|
||||
}
|
||||
|
||||
function getDateFromClickEvent(event) {
|
||||
if (!(event?.target instanceof Element)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dateEl = event.target.closest('[data-vc-date]');
|
||||
if (!(dateEl instanceof HTMLElement)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return dateEl.dataset.vcDate || '';
|
||||
}
|
||||
|
||||
async function initializeCalendar() {
|
||||
if (isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = labels.loading;
|
||||
|
||||
try {
|
||||
await loadCalendarData();
|
||||
|
||||
const Calendar = window.VanillaCalendarPro?.Calendar;
|
||||
if (typeof Calendar !== 'function') {
|
||||
throw new Error('Vanilla Calendar Pro is unavailable');
|
||||
}
|
||||
|
||||
const initialYearMonth = parseInitialYearMonth();
|
||||
const calendarOptions = {
|
||||
...(Number.isInteger(initialYearMonth.selectedYear) ? { selectedYear: initialYearMonth.selectedYear } : {}),
|
||||
...(Number.isInteger(initialYearMonth.selectedMonth) ? { selectedMonth: initialYearMonth.selectedMonth } : {}),
|
||||
onCreateDateEls(_self, dateEl) {
|
||||
const dateKey = dateEl.dataset.vcDate || '';
|
||||
const count = Number(days[dateKey] || 0);
|
||||
const buttonEl = dateEl.querySelector('[data-vc-date-btn]');
|
||||
|
||||
if (!(buttonEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (count <= 0) {
|
||||
dateEl.removeAttribute('data-blog-calendar-has-posts');
|
||||
applyHeatStyle(buttonEl, 0, maxDayCount);
|
||||
return;
|
||||
}
|
||||
|
||||
dateEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||
applyHeatStyle(buttonEl, count, maxDayCount);
|
||||
},
|
||||
onCreateMonthEls(self, monthEl) {
|
||||
if (!(monthEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthIndex = Number(monthEl.dataset.vcMonthsMonth);
|
||||
const selectedYear = Number(self?.context?.selectedYear);
|
||||
if (!Number.isInteger(monthIndex) || !Number.isInteger(selectedYear)) {
|
||||
monthEl.removeAttribute('data-blog-calendar-has-posts');
|
||||
applyHeatStyle(monthEl, 0, maxMonthCount);
|
||||
return;
|
||||
}
|
||||
|
||||
const monthKey = String(selectedYear) + '-' + pad2(monthIndex + 1);
|
||||
const count = Number(months[monthKey] || 0);
|
||||
if (count <= 0) {
|
||||
monthEl.removeAttribute('data-blog-calendar-has-posts');
|
||||
applyHeatStyle(monthEl, 0, maxMonthCount);
|
||||
return;
|
||||
}
|
||||
|
||||
monthEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||
applyHeatStyle(monthEl, count, maxMonthCount);
|
||||
},
|
||||
onCreateYearEls(_self, yearEl) {
|
||||
if (!(yearEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yearValue = Number(yearEl.dataset.vcYearsYear);
|
||||
if (!Number.isInteger(yearValue)) {
|
||||
yearEl.removeAttribute('data-blog-calendar-has-posts');
|
||||
applyHeatStyle(yearEl, 0, maxYearCount);
|
||||
return;
|
||||
}
|
||||
|
||||
const yearKey = String(yearValue);
|
||||
const count = Number(years[yearKey] || 0);
|
||||
if (count <= 0) {
|
||||
yearEl.removeAttribute('data-blog-calendar-has-posts');
|
||||
applyHeatStyle(yearEl, 0, maxYearCount);
|
||||
return;
|
||||
}
|
||||
|
||||
yearEl.setAttribute('data-blog-calendar-has-posts', 'true');
|
||||
applyHeatStyle(yearEl, count, maxYearCount);
|
||||
},
|
||||
onClickDate(_self, event) {
|
||||
const dateKey = getDateFromClickEvent(event);
|
||||
if (!dateKey || !days[dateKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [year, month, day] = dateKey.split('-');
|
||||
if (!year || !month || !day) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/' + year + '/' + month + '/' + day + '/');
|
||||
},
|
||||
onClickMonth(self) {
|
||||
const selectedYear = Number(self?.context?.selectedYear);
|
||||
const selectedMonth = Number(self?.context?.selectedMonth);
|
||||
|
||||
if (!Number.isInteger(selectedYear) || !Number.isInteger(selectedMonth)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const monthKey = String(selectedYear) + '-' + pad2(selectedMonth + 1);
|
||||
if (!months[monthKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/' + String(selectedYear) + '/' + pad2(selectedMonth + 1) + '/');
|
||||
},
|
||||
onClickYear(self) {
|
||||
const selectedYear = Number(self?.context?.selectedYear);
|
||||
|
||||
if (!Number.isInteger(selectedYear)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yearKey = String(selectedYear);
|
||||
if (!years[yearKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigateTo('/' + String(selectedYear) + '/');
|
||||
},
|
||||
};
|
||||
|
||||
const calendar = new Calendar('[data-blog-calendar-root]', calendarOptions);
|
||||
|
||||
calendar.init();
|
||||
status.textContent = '';
|
||||
status.setAttribute('hidden', 'hidden');
|
||||
isInitialized = true;
|
||||
} catch {
|
||||
status.textContent = labels.error;
|
||||
status.removeAttribute('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function setPanelOpen(isOpen) {
|
||||
if (isOpen) {
|
||||
panel.removeAttribute('hidden');
|
||||
void initializeCalendar();
|
||||
return;
|
||||
}
|
||||
|
||||
panel.setAttribute('hidden', 'hidden');
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
const isHidden = panel.hasAttribute('hidden');
|
||||
setPanelOpen(isHidden);
|
||||
});
|
||||
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', () => {
|
||||
setPanelOpen(false);
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
try {
|
||||
return document.execCommand('copy');
|
||||
} catch (_) {
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
@@ -43,7 +43,7 @@
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch {
|
||||
return fallbackCopy(value);
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@
|
||||
try {
|
||||
highlighter.highlightElement(codeElement);
|
||||
codeElement.setAttribute('data-code-highlighted', 'true');
|
||||
} catch (_) {
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,4 +133,4 @@
|
||||
} else {
|
||||
initCodeBlocks();
|
||||
}
|
||||
})();
|
||||
})();
|
||||
505
priv/preview_assets/assets/d3.layout.cloud.js
Normal file
505
priv/preview_assets/assets/d3.layout.cloud.js
Normal file
@@ -0,0 +1,505 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 = {}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
||||
// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
|
||||
// Algorithm due to Jonathan Feinberg, https://s3.amazonaws.com/static.mrfeinberg.com/bv_ch03.pdf
|
||||
|
||||
const dispatch = require("d3-dispatch").dispatch;
|
||||
|
||||
const RADIANS = Math.PI / 180;
|
||||
|
||||
const SPIRALS = {
|
||||
archimedean: archimedeanSpiral,
|
||||
rectangular: rectangularSpiral
|
||||
};
|
||||
|
||||
const cw = 1 << 11 >> 5;
|
||||
const ch = 1 << 11;
|
||||
|
||||
module.exports = function() {
|
||||
var size = [256, 256],
|
||||
text = cloudText,
|
||||
font = cloudFont,
|
||||
fontSize = cloudFontSize,
|
||||
fontStyle = cloudFontNormal,
|
||||
fontWeight = cloudFontNormal,
|
||||
rotate = cloudRotate,
|
||||
padding = cloudPadding,
|
||||
spiral = archimedeanSpiral,
|
||||
words = [],
|
||||
timeInterval = Infinity,
|
||||
event = dispatch("word", "end"),
|
||||
timer = null,
|
||||
random = Math.random,
|
||||
cloud = {},
|
||||
canvas = cloudCanvas;
|
||||
|
||||
cloud.canvas = function(_) {
|
||||
return arguments.length ? (canvas = functor(_), cloud) : canvas;
|
||||
};
|
||||
|
||||
cloud.start = function() {
|
||||
var contextAndRatio = getContext(canvas()),
|
||||
board = zeroArray((size[0] >> 5) * size[1]),
|
||||
bounds = null,
|
||||
n = words.length,
|
||||
i = -1,
|
||||
tags = [],
|
||||
data = words.map(function(d, i) {
|
||||
d.text = text.call(this, d, i);
|
||||
d.font = font.call(this, d, i);
|
||||
d.style = fontStyle.call(this, d, i);
|
||||
d.weight = fontWeight.call(this, d, i);
|
||||
d.rotate = rotate.call(this, d, i);
|
||||
d.size = ~~fontSize.call(this, d, i);
|
||||
d.padding = padding.call(this, d, i);
|
||||
return d;
|
||||
}).sort(function(a, b) { return b.size - a.size; });
|
||||
|
||||
if (timer) clearInterval(timer);
|
||||
timer = setInterval(step, 0);
|
||||
step();
|
||||
|
||||
return cloud;
|
||||
|
||||
function step() {
|
||||
var start = Date.now();
|
||||
while (Date.now() - start < timeInterval && ++i < n && timer) {
|
||||
var d = data[i];
|
||||
d.x = (size[0] * (random() + .5)) >> 1;
|
||||
d.y = (size[1] * (random() + .5)) >> 1;
|
||||
cloudSprite(contextAndRatio, d, data, i);
|
||||
if (d.hasText && place(board, d, bounds)) {
|
||||
tags.push(d);
|
||||
event.call("word", cloud, d);
|
||||
if (bounds) cloudBounds(bounds, d);
|
||||
else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
|
||||
// Temporary hack
|
||||
d.x -= size[0] >> 1;
|
||||
d.y -= size[1] >> 1;
|
||||
}
|
||||
}
|
||||
if (i >= n) {
|
||||
cloud.stop();
|
||||
event.call("end", cloud, tags, bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cloud.stop = function() {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
for (const d of words) {
|
||||
delete d.sprite;
|
||||
}
|
||||
return cloud;
|
||||
};
|
||||
|
||||
function getContext(canvas) {
|
||||
const context = canvas.getContext("2d", {willReadFrequently: true});
|
||||
|
||||
canvas.width = canvas.height = 1;
|
||||
const ratio = Math.sqrt(context.getImageData(0, 0, 1, 1).data.length >> 2);
|
||||
canvas.width = (cw << 5) / ratio;
|
||||
canvas.height = ch / ratio;
|
||||
|
||||
context.fillStyle = context.strokeStyle = "red";
|
||||
|
||||
return {context, ratio};
|
||||
}
|
||||
|
||||
function place(board, tag, bounds) {
|
||||
var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
|
||||
startX = tag.x,
|
||||
startY = tag.y,
|
||||
maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
|
||||
s = spiral(size),
|
||||
dt = random() < .5 ? 1 : -1,
|
||||
t = -dt,
|
||||
dxdy,
|
||||
dx,
|
||||
dy;
|
||||
|
||||
while (dxdy = s(t += dt)) {
|
||||
dx = ~~dxdy[0];
|
||||
dy = ~~dxdy[1];
|
||||
|
||||
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
|
||||
|
||||
tag.x = startX + dx;
|
||||
tag.y = startY + dy;
|
||||
|
||||
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
|
||||
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
|
||||
// TODO only check for collisions within current bounds.
|
||||
if (!bounds || collideRects(tag, bounds)) {
|
||||
if (!cloudCollide(tag, board, size[0])) {
|
||||
var sprite = tag.sprite,
|
||||
w = tag.width >> 5,
|
||||
sw = size[0] >> 5,
|
||||
lx = tag.x - (w << 4),
|
||||
sx = lx & 0x7f,
|
||||
msx = 32 - sx,
|
||||
h = tag.y1 - tag.y0,
|
||||
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||
last;
|
||||
for (var j = 0; j < h; j++) {
|
||||
last = 0;
|
||||
for (var i = 0; i <= w; i++) {
|
||||
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
|
||||
}
|
||||
x += sw;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
cloud.timeInterval = function(_) {
|
||||
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
|
||||
};
|
||||
|
||||
cloud.words = function(_) {
|
||||
return arguments.length ? (words = _, cloud) : words;
|
||||
};
|
||||
|
||||
cloud.size = function(_) {
|
||||
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
|
||||
};
|
||||
|
||||
cloud.font = function(_) {
|
||||
return arguments.length ? (font = functor(_), cloud) : font;
|
||||
};
|
||||
|
||||
cloud.fontStyle = function(_) {
|
||||
return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
|
||||
};
|
||||
|
||||
cloud.fontWeight = function(_) {
|
||||
return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
|
||||
};
|
||||
|
||||
cloud.rotate = function(_) {
|
||||
return arguments.length ? (rotate = functor(_), cloud) : rotate;
|
||||
};
|
||||
|
||||
cloud.text = function(_) {
|
||||
return arguments.length ? (text = functor(_), cloud) : text;
|
||||
};
|
||||
|
||||
cloud.spiral = function(_) {
|
||||
return arguments.length ? (spiral = SPIRALS[_] || _, cloud) : spiral;
|
||||
};
|
||||
|
||||
cloud.fontSize = function(_) {
|
||||
return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
|
||||
};
|
||||
|
||||
cloud.padding = function(_) {
|
||||
return arguments.length ? (padding = functor(_), cloud) : padding;
|
||||
};
|
||||
|
||||
cloud.random = function(_) {
|
||||
return arguments.length ? (random = _, cloud) : random;
|
||||
};
|
||||
|
||||
cloud.on = function() {
|
||||
var value = event.on.apply(event, arguments);
|
||||
return value === event ? cloud : value;
|
||||
};
|
||||
|
||||
return cloud;
|
||||
};
|
||||
|
||||
function cloudText(d) {
|
||||
return d.text;
|
||||
}
|
||||
|
||||
function cloudFont() {
|
||||
return "serif";
|
||||
}
|
||||
|
||||
function cloudFontNormal() {
|
||||
return "normal";
|
||||
}
|
||||
|
||||
function cloudFontSize(d) {
|
||||
return Math.sqrt(d.value);
|
||||
}
|
||||
|
||||
function cloudRotate() {
|
||||
return (~~(random() * 6) - 3) * 30;
|
||||
}
|
||||
|
||||
function cloudPadding() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fetches a monochrome sprite bitmap for the specified text.
|
||||
// Load in batches for speed.
|
||||
function cloudSprite(contextAndRatio, d, data, di) {
|
||||
if (d.sprite) return;
|
||||
var c = contextAndRatio.context,
|
||||
ratio = contextAndRatio.ratio;
|
||||
|
||||
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
|
||||
var x = 0,
|
||||
y = 0,
|
||||
maxh = 0,
|
||||
n = data.length;
|
||||
--di;
|
||||
while (++di < n) {
|
||||
d = data[di];
|
||||
c.save();
|
||||
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
|
||||
const metrics = c.measureText(d.text);
|
||||
const anchor = -Math.floor(metrics.width / 2);
|
||||
let w = (metrics.width + 1) * ratio;
|
||||
let h = d.size << 1;
|
||||
if (d.rotate) {
|
||||
var sr = Math.sin(d.rotate * RADIANS),
|
||||
cr = Math.cos(d.rotate * RADIANS),
|
||||
wcr = w * cr,
|
||||
wsr = w * sr,
|
||||
hcr = h * cr,
|
||||
hsr = h * sr;
|
||||
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
|
||||
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
|
||||
} else {
|
||||
w = (w + 0x1f) >> 5 << 5;
|
||||
}
|
||||
if (h > maxh) maxh = h;
|
||||
if (x + w >= (cw << 5)) {
|
||||
x = 0;
|
||||
y += maxh;
|
||||
maxh = 0;
|
||||
}
|
||||
if (y + h >= ch) break;
|
||||
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
|
||||
if (d.rotate) c.rotate(d.rotate * RADIANS);
|
||||
c.fillText(d.text, anchor, 0);
|
||||
if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, anchor, 0);
|
||||
c.restore();
|
||||
d.width = w;
|
||||
d.height = h;
|
||||
d.xoff = x;
|
||||
d.yoff = y;
|
||||
d.x1 = w >> 1;
|
||||
d.y1 = h >> 1;
|
||||
d.x0 = -d.x1;
|
||||
d.y0 = -d.y1;
|
||||
d.hasText = true;
|
||||
x += w;
|
||||
}
|
||||
var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data,
|
||||
sprite = [];
|
||||
while (--di >= 0) {
|
||||
d = data[di];
|
||||
if (!d.hasText) continue;
|
||||
var w = d.width,
|
||||
w32 = w >> 5,
|
||||
h = d.y1 - d.y0;
|
||||
// Zero the buffer
|
||||
for (var i = 0; i < h * w32; i++) sprite[i] = 0;
|
||||
x = d.xoff;
|
||||
if (x == null) return;
|
||||
y = d.yoff;
|
||||
var seen = 0,
|
||||
seenRow = -1;
|
||||
for (var j = 0; j < h; j++) {
|
||||
for (var i = 0; i < w; i++) {
|
||||
var k = w32 * j + (i >> 5),
|
||||
m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
|
||||
sprite[k] |= m;
|
||||
seen |= m;
|
||||
}
|
||||
if (seen) seenRow = j;
|
||||
else {
|
||||
d.y0++;
|
||||
h--;
|
||||
j--;
|
||||
y++;
|
||||
}
|
||||
}
|
||||
d.y1 = d.y0 + seenRow;
|
||||
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
|
||||
}
|
||||
}
|
||||
|
||||
// Use mask-based collision detection.
|
||||
function cloudCollide(tag, board, sw) {
|
||||
sw >>= 5;
|
||||
var sprite = tag.sprite,
|
||||
w = tag.width >> 5,
|
||||
lx = tag.x - (w << 4),
|
||||
sx = lx & 0x7f,
|
||||
msx = 32 - sx,
|
||||
h = tag.y1 - tag.y0,
|
||||
x = (tag.y + tag.y0) * sw + (lx >> 5),
|
||||
last;
|
||||
for (var j = 0; j < h; j++) {
|
||||
last = 0;
|
||||
for (var i = 0; i <= w; i++) {
|
||||
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
|
||||
& board[x + i]) return true;
|
||||
}
|
||||
x += sw;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function cloudBounds(bounds, d) {
|
||||
var b0 = bounds[0],
|
||||
b1 = bounds[1];
|
||||
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
|
||||
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
|
||||
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
|
||||
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
|
||||
}
|
||||
|
||||
function collideRects(a, b) {
|
||||
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
|
||||
}
|
||||
|
||||
function archimedeanSpiral(size) {
|
||||
var e = size[0] / size[1];
|
||||
return function(t) {
|
||||
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
|
||||
};
|
||||
}
|
||||
|
||||
function rectangularSpiral(size) {
|
||||
var dy = 4,
|
||||
dx = dy * size[0] / size[1],
|
||||
x = 0,
|
||||
y = 0;
|
||||
return function(t) {
|
||||
var sign = t < 0 ? -1 : 1;
|
||||
// See triangular numbers: T_n = n * (n + 1) / 2.
|
||||
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
|
||||
case 0: x += dx; break;
|
||||
case 1: y += dy; break;
|
||||
case 2: x -= dx; break;
|
||||
default: y -= dy; break;
|
||||
}
|
||||
return [x, y];
|
||||
};
|
||||
}
|
||||
|
||||
// TODO reuse arrays?
|
||||
function zeroArray(n) {
|
||||
var a = [],
|
||||
i = -1;
|
||||
while (++i < n) a[i] = 0;
|
||||
return a;
|
||||
}
|
||||
|
||||
function cloudCanvas() {
|
||||
return document.createElement("canvas");
|
||||
}
|
||||
|
||||
function functor(d) {
|
||||
return typeof d === "function" ? d : function() { return d; };
|
||||
}
|
||||
|
||||
},{"d3-dispatch":2}],2:[function(require,module,exports){
|
||||
// https://d3js.org/d3-dispatch/ v1.0.6 Copyright 2019 Mike Bostock
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(global = global || self, factory(global.d3 = global.d3 || {}));
|
||||
}(this, function (exports) { 'use strict';
|
||||
|
||||
var noop = {value: function() {}};
|
||||
|
||||
function dispatch() {
|
||||
for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {
|
||||
if (!(t = arguments[i] + "") || (t in _) || /[\s.]/.test(t)) throw new Error("illegal type: " + t);
|
||||
_[t] = [];
|
||||
}
|
||||
return new Dispatch(_);
|
||||
}
|
||||
|
||||
function Dispatch(_) {
|
||||
this._ = _;
|
||||
}
|
||||
|
||||
function parseTypenames(typenames, types) {
|
||||
return typenames.trim().split(/^|\s+/).map(function(t) {
|
||||
var name = "", i = t.indexOf(".");
|
||||
if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
|
||||
if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t);
|
||||
return {type: t, name: name};
|
||||
});
|
||||
}
|
||||
|
||||
Dispatch.prototype = dispatch.prototype = {
|
||||
constructor: Dispatch,
|
||||
on: function(typename, callback) {
|
||||
var _ = this._,
|
||||
T = parseTypenames(typename + "", _),
|
||||
t,
|
||||
i = -1,
|
||||
n = T.length;
|
||||
|
||||
// If no callback was specified, return the callback of the given type and name.
|
||||
if (arguments.length < 2) {
|
||||
while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;
|
||||
return;
|
||||
}
|
||||
|
||||
// If a type was specified, set the callback for the given type and name.
|
||||
// Otherwise, if a null callback was specified, remove callbacks of the given name.
|
||||
if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback);
|
||||
while (++i < n) {
|
||||
if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);
|
||||
else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
copy: function() {
|
||||
var copy = {}, _ = this._;
|
||||
for (var t in _) copy[t] = _[t].slice();
|
||||
return new Dispatch(copy);
|
||||
},
|
||||
call: function(type, that) {
|
||||
if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];
|
||||
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
|
||||
for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
|
||||
},
|
||||
apply: function(type, that, args) {
|
||||
if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type);
|
||||
for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);
|
||||
}
|
||||
};
|
||||
|
||||
function get(type, name) {
|
||||
for (var i = 0, n = type.length, c; i < n; ++i) {
|
||||
if ((c = type[i]).name === name) {
|
||||
return c.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function set(type, name, callback) {
|
||||
for (var i = 0, n = type.length; i < n; ++i) {
|
||||
if (type[i].name === name) {
|
||||
type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (callback != null) type.push({name: name, value: callback});
|
||||
return type;
|
||||
}
|
||||
|
||||
exports.dispatch = dispatch;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
}));
|
||||
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
10
priv/preview_assets/assets/highlight.min.css
vendored
Normal file
10
priv/preview_assets/assets/highlight.min.css
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||
Theme: GitHub Dark
|
||||
Description: Dark theme as seen on github.com
|
||||
Author: github.com
|
||||
Maintainer: @Hirse
|
||||
Updated: 2021-05-15
|
||||
|
||||
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||
Current colors taken from GitHub's CSS
|
||||
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||
1244
priv/preview_assets/assets/highlight.min.js
vendored
Normal file
1244
priv/preview_assets/assets/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
priv/preview_assets/assets/lightbox.min.css
vendored
Normal file
1
priv/preview_assets/assets/lightbox.min.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.lb-loader,.lightbox{text-align:center;line-height:0}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;opacity:.8;display:none}.lightbox{position:absolute;left:0;width:100%;z-index:10000;font-weight:400;outline:0}.lightbox .lb-image{display:block;height:auto;max-width:inherit;max-height:none;border-radius:3px;border:4px solid #fff}.lightbox a img{border:none}.lb-outerContainer{position:relative;width:250px;height:250px;margin:0 auto;border-radius:4px;background-color:#fff}.lb-loader,.lb-nav{position:absolute;left:0}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-loader{top:43%;height:25%;width:100%}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{top:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:0;background-image:url(data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-caption a{color:#4ae}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) top right no-repeat;text-align:right;outline:0;opacity:.7;-webkit-transition:opacity .2s;-moz-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;opacity:1}
|
||||
25
priv/preview_assets/assets/lightbox.min.js
vendored
Normal file
25
priv/preview_assets/assets/lightbox.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.amber.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.amber.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.blue.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.blue.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.cyan.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.cyan.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.fuchsia.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.fuchsia.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.green.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.green.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.grey.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.grey.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.indigo.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.indigo.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.jade.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.jade.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.lime.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.lime.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.orange.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.orange.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.pink.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.pink.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.pumpkin.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.pumpkin.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.purple.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.purple.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.red.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.red.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.sand.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.sand.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.slate.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.slate.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.violet.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.violet.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.yellow.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.yellow.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
priv/preview_assets/assets/pico.zinc.min.css
vendored
Normal file
4
priv/preview_assets/assets/pico.zinc.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
52
priv/preview_assets/assets/search-runtime.js
Normal file
52
priv/preview_assets/assets/search-runtime.js
Normal file
@@ -0,0 +1,52 @@
|
||||
(() => {
|
||||
const toggle = document.querySelector('[data-blog-search-toggle]');
|
||||
const panel = document.querySelector('[data-blog-search-panel]');
|
||||
const root = document.querySelector('[data-blog-search-root]');
|
||||
|
||||
if (!toggle || !panel || !root) {
|
||||
return;
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
|
||||
function initSearch() {
|
||||
if (initialized || typeof PagefindUI === 'undefined') {
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
var placeholder = root.getAttribute('data-search-placeholder') || 'Search...';
|
||||
new PagefindUI({
|
||||
element: root,
|
||||
showSubResults: true,
|
||||
showImages: false,
|
||||
translations: { placeholder: placeholder }
|
||||
});
|
||||
var input = root.querySelector('input');
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', function() {
|
||||
var isHidden = panel.hasAttribute('hidden');
|
||||
if (isHidden) {
|
||||
panel.removeAttribute('hidden');
|
||||
initSearch();
|
||||
} else {
|
||||
panel.setAttribute('hidden', '');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!panel.hasAttribute('hidden') && !panel.contains(e.target) && !toggle.contains(e.target)) {
|
||||
panel.setAttribute('hidden', '');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && !panel.hasAttribute('hidden')) {
|
||||
panel.setAttribute('hidden', '');
|
||||
toggle.focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
272
priv/preview_assets/assets/tag-cloud.js
Normal file
272
priv/preview_assets/assets/tag-cloud.js
Normal file
@@ -0,0 +1,272 @@
|
||||
(function () {
|
||||
function parseWords(rawWords) {
|
||||
if (!rawWords || typeof rawWords !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawWords);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function clamp01(value) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (value > 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseCssColor(colorValue) {
|
||||
if (typeof colorValue !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = colorValue.trim();
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
||||
if (hexMatch) {
|
||||
const hex = hexMatch[1];
|
||||
if (hex.length === 3) {
|
||||
return [
|
||||
Number.parseInt(hex[0] + hex[0], 16),
|
||||
Number.parseInt(hex[1] + hex[1], 16),
|
||||
Number.parseInt(hex[2] + hex[2], 16),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
Number.parseInt(hex.slice(0, 2), 16),
|
||||
Number.parseInt(hex.slice(2, 4), 16),
|
||||
Number.parseInt(hex.slice(4, 6), 16),
|
||||
];
|
||||
}
|
||||
|
||||
const rgbMatch = value.match(/^rgba?\\(([^)]+)\\)$/i);
|
||||
if (rgbMatch) {
|
||||
const channels = rgbMatch[1]
|
||||
.split(',')
|
||||
.map((channel) => channel.trim())
|
||||
.slice(0, 3)
|
||||
.map((channel) => {
|
||||
if (channel.endsWith('%')) {
|
||||
return Math.round((Number.parseFloat(channel) / 100) * 255);
|
||||
}
|
||||
|
||||
return Math.round(Number.parseFloat(channel));
|
||||
});
|
||||
|
||||
if (channels.length === 3 && channels.every((channel) => Number.isFinite(channel))) {
|
||||
return channels.map((channel) => Math.max(0, Math.min(255, channel)));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function interpolateColor(fromColor, toColor, t) {
|
||||
return [
|
||||
Math.round(fromColor[0] + ((toColor[0] - fromColor[0]) * t)),
|
||||
Math.round(fromColor[1] + ((toColor[1] - fromColor[1]) * t)),
|
||||
Math.round(fromColor[2] + ((toColor[2] - fromColor[2]) * t)),
|
||||
];
|
||||
}
|
||||
|
||||
function mixColor(fromColor, toColor, weight) {
|
||||
return interpolateColor(fromColor, toColor, clamp01(weight));
|
||||
}
|
||||
|
||||
function colorToCss(color) {
|
||||
return 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
|
||||
}
|
||||
|
||||
function getPicoThemeStops() {
|
||||
const style = window.getComputedStyle(document.documentElement);
|
||||
|
||||
const blue = parseCssColor(style.getPropertyValue('--pico-secondary')) || [74, 99, 146];
|
||||
const green = parseCssColor(style.getPropertyValue('--pico-ins-color')) || [53, 117, 56];
|
||||
const red = parseCssColor(style.getPropertyValue('--pico-del-color')) || [183, 72, 72];
|
||||
|
||||
const yellow = mixColor(green, red, 0.45);
|
||||
const orange = mixColor(green, red, 0.72);
|
||||
|
||||
return [blue, green, yellow, orange, red];
|
||||
}
|
||||
|
||||
function interpolateStops(stops, value) {
|
||||
if (!Array.isArray(stops) || stops.length === 0) {
|
||||
return 'currentColor';
|
||||
}
|
||||
|
||||
if (stops.length === 1) {
|
||||
return colorToCss(stops[0]);
|
||||
}
|
||||
|
||||
const clamped = clamp01(value);
|
||||
const scaled = clamped * (stops.length - 1);
|
||||
const lowerIndex = Math.floor(scaled);
|
||||
const upperIndex = Math.min(stops.length - 1, lowerIndex + 1);
|
||||
const localT = scaled - lowerIndex;
|
||||
|
||||
return colorToCss(interpolateColor(stops[lowerIndex], stops[upperIndex], localT));
|
||||
}
|
||||
|
||||
function resolveQuantileColorMap(words) {
|
||||
const counts = Array.from(
|
||||
new Set(words.map((word) => Number(word.count)).filter((count) => Number.isFinite(count)))
|
||||
).sort((a, b) => a - b);
|
||||
|
||||
const quantiles = new Map();
|
||||
if (counts.length === 0) {
|
||||
return quantiles;
|
||||
}
|
||||
|
||||
if (counts.length === 1) {
|
||||
quantiles.set(counts[0], 1);
|
||||
return quantiles;
|
||||
}
|
||||
|
||||
counts.forEach((count, index) => {
|
||||
quantiles.set(count, index / (counts.length - 1));
|
||||
});
|
||||
|
||||
return quantiles;
|
||||
}
|
||||
|
||||
function applyThemeAwareColors(words, container) {
|
||||
const gammaRaw = Number.parseFloat(container.getAttribute('data-color-easing') || '0.7');
|
||||
const gamma = Number.isFinite(gammaRaw) && gammaRaw > 0 ? gammaRaw : 0.7;
|
||||
const quantiles = resolveQuantileColorMap(words);
|
||||
const stops = getPicoThemeStops();
|
||||
|
||||
return words.map((word) => {
|
||||
const count = Number(word.count);
|
||||
const quantile = quantiles.get(count) ?? 0;
|
||||
const eased = Math.pow(clamp01(quantile), gamma);
|
||||
|
||||
return {
|
||||
...word,
|
||||
color: interpolateStops(stops, eased),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function drawTagCloud(container) {
|
||||
const cloudFactory = window.d3 && window.d3.layout && typeof window.d3.layout.cloud === 'function'
|
||||
? window.d3.layout.cloud
|
||||
: null;
|
||||
|
||||
if (!cloudFactory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const langPrefix = document.documentElement.getAttribute('data-language-prefix') || '';
|
||||
const rawWords = container.getAttribute('data-tag-cloud-words');
|
||||
const words = parseWords(rawWords);
|
||||
if (words.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const colorDistribution = container.getAttribute('data-color-distribution') || 'quantile';
|
||||
const colorTheme = container.getAttribute('data-color-theme') || 'pico';
|
||||
const coloredWords = colorDistribution === 'quantile' && colorTheme === 'pico'
|
||||
? applyThemeAwareColors(words, container)
|
||||
: words;
|
||||
|
||||
const width = Number.parseInt(container.getAttribute('data-width') || '900', 10) || 900;
|
||||
const height = Number.parseInt(container.getAttribute('data-height') || '420', 10) || 420;
|
||||
const orientation = container.getAttribute('data-orientation') || 'horizontal';
|
||||
|
||||
const resolveRotation = () => {
|
||||
if (orientation === 'mixed-hv') {
|
||||
return Math.random() < 0.5 ? 0 : 90;
|
||||
}
|
||||
|
||||
if (orientation === 'mixed-diagonal') {
|
||||
const diagonalAngles = [-60, -30, 0, 30, 60, 90];
|
||||
const index = Math.floor(Math.random() * diagonalAngles.length);
|
||||
return diagonalAngles[index];
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const svgNode = container.querySelector('svg.tag-cloud-canvas');
|
||||
if (!svgNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (svgNode.firstChild) {
|
||||
svgNode.removeChild(svgNode.firstChild);
|
||||
}
|
||||
|
||||
cloudFactory()
|
||||
.size([width, height])
|
||||
.words(coloredWords.map((word) => ({ ...word })))
|
||||
.padding(4)
|
||||
.rotate(() => resolveRotation())
|
||||
.font('sans-serif')
|
||||
.fontSize((word) => word.size)
|
||||
.on('end', (layoutWords) => {
|
||||
svgNode.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
|
||||
svgNode.setAttribute('preserveAspectRatio', 'xMidYMid meet');
|
||||
|
||||
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
group.setAttribute('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')');
|
||||
|
||||
for (const word of layoutWords) {
|
||||
const textNode = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
textNode.textContent = word.text;
|
||||
textNode.setAttribute('text-anchor', 'middle');
|
||||
textNode.setAttribute('transform', 'translate(' + word.x + ',' + word.y + ')rotate(' + (word.rotate || 0) + ')');
|
||||
textNode.style.fontFamily = 'sans-serif';
|
||||
textNode.style.fontSize = word.size + 'px';
|
||||
textNode.style.fill = typeof word.color === 'string' && word.color.length > 0
|
||||
? word.color
|
||||
: 'currentColor';
|
||||
textNode.style.cursor = 'pointer';
|
||||
textNode.style.opacity = '0.9';
|
||||
|
||||
const titleNode = document.createElementNS('http://www.w3.org/2000/svg', 'title');
|
||||
titleNode.textContent = word.text + ' (' + word.count + ')';
|
||||
textNode.appendChild(titleNode);
|
||||
|
||||
textNode.addEventListener('click', () => {
|
||||
if (word && typeof word.url === 'string' && word.url.length > 0) {
|
||||
window.location.assign(langPrefix + word.url);
|
||||
}
|
||||
});
|
||||
|
||||
group.appendChild(textNode);
|
||||
}
|
||||
|
||||
svgNode.appendChild(group);
|
||||
})
|
||||
.start();
|
||||
}
|
||||
|
||||
function initTagClouds() {
|
||||
const containers = document.querySelectorAll('[data-tag-cloud="true"]');
|
||||
containers.forEach((container) => drawTagCloud(container));
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initTagClouds, { once: true });
|
||||
} else {
|
||||
initTagClouds();
|
||||
}
|
||||
})();
|
||||
1
priv/preview_assets/assets/vanilla-calendar.min.css
vendored
Normal file
1
priv/preview_assets/assets/vanilla-calendar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
priv/preview_assets/assets/vanilla-calendar.min.js
vendored
Normal file
2
priv/preview_assets/assets/vanilla-calendar.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
priv/preview_assets/images/close.png
Normal file
BIN
priv/preview_assets/images/close.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 B |
BIN
priv/preview_assets/images/loading.gif
Normal file
BIN
priv/preview_assets/images/loading.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
priv/preview_assets/images/next.png
Normal file
BIN
priv/preview_assets/images/next.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
priv/preview_assets/images/prev.png
Normal file
BIN
priv/preview_assets/images/prev.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user