yearly backup

This commit is contained in:
flupe 2024-02-15 18:56:40 +01:00
parent 84ea2d597b
commit 3309bdd66c
28 changed files with 712 additions and 34 deletions

0
.gitignore vendored Normal file → Executable file
View File

25
README.md Normal file → Executable file
View File

@ -5,14 +5,19 @@ nix-shell --attr env release.nix
nix-env -if release.nix
```
## todo
- [ ] custom math rendering (to MathML Core)
- [ ] better agda literate support
- [x] dark theme
- [x] generic feed generation
- [x] draft builds + live server
(draft builds with `-D`, live server with [slod])
- [x] bin packing / grid system for galery
(uses `masonry` css-grid value, only supported in firefox under a flag)
- [ ] faster thumbnail generation with openCV
- [ ] indieweb interactions (webmentions, etc)?
- [ ] better gallery (albums, webzines, media types, layouts, etc)
- [ ] tag/category/search engine
- [ ] parallelization
- [ ] remove pandoc and use custom (extensible-ish) solution
- dark theme
- faster thumbnail generation with openCV
- generic feed generation
- indieweb interactions (webmentions, etc)
- bin packing / grid system for galery
- better gallery (albums, webzines, media types, layouts, etc)
- tag/category/search engine
- parallelization
- draft builds + live server
[slod]: https://acatalepsie.fr/projects/slod

1
archive.sh Executable file
View File

@ -0,0 +1 @@
tar -C _site -cvz . > site.tar.gz

0
content/assets/favicon.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 267 B

After

Width:  |  Height:  |  Size: 267 B

View File

@ -0,0 +1,248 @@
const {cos, sin, min, max, atan2, acos, sqrt, abs, PI} = Math
const clamp = (a, b, x) => x < a ? a : x > b ? b : x
const on = (t, e, f) => t.addEventListener(e, f)
const ons = (t, es, f) => es.forEach(e => t.addEventListener(e, f))
const canvasses = {}
const ratio = devicePixelRatio
const vec = (x = 0, y = 0, z = 0) => ({x, y, z})
vec.set = (o, x = 0, y = 0, z = 0) => {
o.x = x
o.y = y
o.z = z
return o
}
const X = vec(1, 0, 0)
const Y = vec(0, 1, 0)
const Z = vec(0, 0, 1)
// vec used for intermediate results
// avoids creating a bunch of new objects in loops
const _V = vec()
// prevent out of view canvasses from animating
const observer = new IntersectionObserver((entries, obv) =>
entries.forEach(({ target, isIntersecting }) => {
let canvas = canvasses[target.id]
canvas.visible = isIntersecting
if (canvas.animate) {
if (entry.isIntersecting) canvas.step()
else cancelAnimationFrame(canvas.stepId)
}
else if (canvas.mustRedraw && isIntersecting) {
canvas.render()
canvas.mustRedraw = false
}
}), {})
// project point on orbital camera
function project(o, {theta, phi}, {x, y, z}) {
let ct = cos(theta), st = sin(theta)
let cp = cos(phi), sp = sin(phi)
let a = x * ct + y * st
o.x = y * ct - x * st
o.y = cp * z - sp * a
o.z = cp * a + sp * z
return o
}
// adaptative color
const palette = {
red : ['#f00', '#f0f'],
green : ['#0f0', '#0f0'],
blue : ['#00f', '#0ff'],
cyan : ['#0ff', '#00f'],
yellow : ['#f8c325', '#f8c325'],
dark : ['#333', '#fff'],
light : ['#bbb', '#666'],
}
let colorscheme = matchMedia('(prefers-color-scheme: dark)').matches || 'light'
const color = name => palette[name][colorscheme == 'light' ? 0 : 1]
on(matchMedia('(prefers-color-scheme: dark)'), 'change', ({matches}) => {
colorscheme = matches
console.log(colorscheme)
Object.values(canvasses).forEach(cvs => {
if (cvs.animate) return
if (cvs.visible) cvs.render()
else cvs.mustRedraw = true
})
})
// generic canvas
class Canvas {
constructor(id, meta) {
this.id = id
this.meta = meta
this.cvs = window[id]
this.ctx = this.cvs.getContext('2d')
this.mustRedraw = false
this.hasFocus = false
if (meta.init) meta.init.call(this)
on(window, 'resize', this.resize.bind(this))
this.resize()
if (meta.inputs) {
meta.inputs.forEach(el => on(el, 'input', this.render.bind(this)))
}
observer.observe(this.cvs)
canvasses[id] = this
if (meta.animate) this.stepId = requestAnimationFrame(this.step.bind(this))
}
resize() {
let width = this.width = this.cvs.clientWidth
let height = this.height = this.cvs.clientHeight
this.cvs.width = ratio * width
this.cvs.height = ratio * height
this.ctx.scale(ratio, ratio)
this.ctx.lineCap = 'round'
this.render()
}
render() {
this.ctx.save()
this.meta.render.call(this, this.ctx)
this.ctx.restore()
}
step() {
this.stepId = requestAnimationFrame(this.step.bind(this))
this.render()
}
drawCircle(x, y, r, c, w = 1) {
let ctx = this.ctx
ctx.strokeStyle = c
ctx.lineWidth = w
ctx.beginPath()
ctx.arc(x, y, r, 0, 2 * PI)
ctx.stroke()
}
drawVec(camera, v, r, c, w = 1) {
let ctx = this.ctx
let {x, y} = project(_V, camera, v)
ctx.lineWidth = w
ctx.strokeStyle = c
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(x * r, y * r)
ctx.stroke()
}
// draw 3d basis at (0, 0), from orbital camera
drawBasis(camera, r) {
this.drawVec(camera, X, r, color('red'), 2)
this.drawVec(camera, Y, r, color('green'), 2)
this.drawVec(camera, Z, r, color('blue'), 2)
}
// given a sphere at (0, 0) of radius R, draw sphere section
// along normal n and offset o, from orbital camera
drawSectionFront(camera, n, R, o = 0) {
let {x, y, z} = project(_V, camera, n) // project normal on camera
let a = atan2(y, x) // angle of projected normal -> angle of ellipse
let ry = sqrt(1 - o * o) // radius of section -> y-radius of ellipse
let rx = ry * abs(z) // x-radius of ellipse
let W = sqrt(x * x + y * y)
let sa = acos(clamp(-1, 1, o * (1 / W - W) / rx || 0)) // ellipse start angle
let sb = z > 0 ? 2 * PI - sa : - sa // ellipse end angle
let ctx = this.ctx
ctx.beginPath()
ctx.ellipse(x * o * R, y * o * R, rx * R, ry * R, a, sa, sb, z <= 0)
ctx.stroke()
}
pathSection(camera, n, R, o = 0) {
let {x, y, z} = project(_V, camera, n)
let a = atan2(y, x)
let ry = sqrt(1 - o * o)
let rx = ry * abs(z)
this.ctx.ellipse(x * o * R, y * o * R, rx * R, ry * R, a, 0, 2 * PI)
}
drawSection(camera, n, R, o, c, w) {
this.ctx.strokeStyle = c
this.ctx.lineWidth = w
this.ctx.beginPath()
this.pathSection(camera, n, R, o)
this.ctx.stroke()
}
drawWireframe(camera, r) {
this.drawSectionFront(camera, X, r)
this.drawSectionFront(camera, Y, r)
this.drawSectionFront(camera, Z, r)
}
drawSections(camera, r) {
this.drawSectionFront(camera, X, r)
this.drawSectionFront(camera, X, r, .5)
this.drawSectionFront(camera, X, r, -.5)
this.drawSectionFront(camera, Y, r)
this.drawSectionFront(camera, Y, r, .5)
this.drawSectionFront(camera, Y, r, -.5)
this.drawSectionFront(camera, Z, r)
this.drawSectionFront(camera, Z, r, .5)
this.drawSectionFront(camera, Z, r, -.5)
}
}
function setup_orbiter(cvs, theta, phi) {
cvs.cvs.classList.toggle('grabber')
// TODO: parametrized scaling
// mouse events
on(cvs.cvs, 'mousedown', e => {
cvs.grabbing = true
cvs.cvs.style.setProperty('cursor', 'grabbing')
document.body.style.setProperty('cursor', 'grabbing')
})
on(window, 'mousemove', e => {
if (!cvs.grabbing) return
e.preventDefault()
phi.value = clamp(-1, 1, parseFloat(phi.value) + e.movementY / 400)
let th = (parseFloat(theta.value) - e.movementX / 800 + 1) % 1
theta.value = th
if (!cvs.animate) cvs.render()
})
on(window, 'mouseup', e => {
if (!cvs.grabbing) return
cvs.cvs.style.removeProperty('cursor')
document.body.style.removeProperty('cursor')
cvs.grabbing = false
})
/* Ok, SO: for the orbiter to work on mobile and NOT interfere with regular scrolling
* we require the canvas to be focused BEFORE (hence tabindex="-1" on the element).
* A bit sad to require one more interaction from the user,
* but I can't stand having interactive canvases prevent me from scrolling so this is the lesser
* of two evils, I hope.
*/
on(cvs.cvs, 'focus', () => cvs.hasFocus = true)
on(cvs.cvs, 'blur', () => cvs.hasFocus = false)
}

0
content/index.rst Normal file → Executable file
View File

View File

@ -0,0 +1,353 @@
---
title: Drawing a wireframe sphere with Canvas2D using ellipses
date: 2022-10-20
draft: true
math: true
---
```{=html}
<script src="/assets/sphere-main.js" defer></script>
```
In order to *visualize* a raycasting algorithm, I recently started writing some
javascript code to render [3D gizmos] using [Canvas2D].
I reached a point where I needed to draw a (unit) **sphere**. A simple mind would
think displaying a *circle* for the circumference of the sphere is enough.
But if we want to make the *rotation* of the sphere *visible* and *obvious*,
it's also important to to display its *wireframe*.
[3D gizmos]: https://twitter.com/sbbls/status/1582473846140960769?s=20&t=K1rByQRkOudayyoWZq5yCQ
[Canvas2D]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
If you click and drag your mouse over the next canvas to rotate the camera
'round the spheres, it should become clear that we can only really grasp the
orientation on the right.
```{=html}
<figure id="split-fig" class="leftbottom">
<input id="split_phi" orient="vertical" type="range" min="-1" step="any" max="1" value=".42">
<canvas id="split" class="r2" tabindex="-1"></canvas>
<input id="split_theta" type="range" min="0" step="any" max="1" value=".125" style="direction: rtl">
</figure>
<style>
.leftbottom {
display: grid;
grid-template: "p c" 1fr
"_ t" auto / auto 1fr;
}
.leftbottom input:not([orient="vertical"]) { grid-area: t}
</style>
<script type="module">
let camera = { theta: 0, phi: 0 }
new Canvas('split', {
inputs: [split_theta, split_phi],
init() { setup_orbiter(this, split_theta, split_phi) },
render(ctx) {
ctx.clearRect(0, 0, this.width, this.height)
ctx.translate(this.width / 2, this.height / 2)
ctx.scale(1, -1)
camera.theta = split_theta.value * 2 * PI
camera.phi = split_phi.value * PI / 2
let x = this.width / 4
let r = min(this.width / 2, this.height) / 2 - 20
let c = color('dark')
ctx.lineCap = 'round'
// left
ctx.translate(- x, 0)
this.drawBasis(camera, r)
this.drawCircle(0, 0, r, c, 4)
// right
ctx.translate(2 * x, 0)
this.drawBasis(camera, r)
this.drawCircle(0, 0, r, c, 4)
ctx.lineCap = 'butt'
this.drawWireframe(camera, r)
}
})
</script>
```
But how de we render such a wireframe of the sphere using only 2D primitives?
The usual technique is to compute the position of all the vertices that compose
the wireframe. Then you compute all the edges. To render the wireframe, project
every vertex to the screen, then stroke a line segment for every edge.
```{=html}
<figure class="leftbottom">
<input id="bad_phi" orient="vertical" type="range" min="-1" step="any" max="1" value=".42">
<canvas id="wire" class="r1 hw" tabindex="-1"></canvas>
<input id="bad_theta" type="range" min="0" step="any" max="1" value=".125" style="direction: rtl">
</figure>
<script type="module">
let camera = { theta: PI / 4, phi: .7 }
let points = [], edges = []
// yes, there are duplicates at poles, no, i do not care
for (let i = 0, c = 6; i <= c; i++) {
let b = i * Math.PI / c
for (let j = 0, d = 8; j < d; j++) {
let a = j / d * Math.PI * 2
points.push(vec(cos(a) * sin(b), sin(a) * sin(b), cos(b)))
edges.push([i * d + j, i * d + ((j + 1) % d)])
if (i > 0) edges.push([i * d + j, (i - 1) * d + j])
}
}
let _points = points.map(_ => vec())
new Canvas('wire', {
inputs: [bad_theta, bad_phi],
init() { setup_orbiter(this, bad_theta, bad_phi) },
render(ctx) {
ctx.clearRect(0, 0, this.width, this.height)
ctx.translate(this.width / 2, this.height / 2)
ctx.scale(1, -1)
ctx.lineCap = 'round'
camera.theta = bad_theta.value * 2 * PI
camera.phi = bad_phi.value * PI / 2
points.forEach((v, i) => project(_points[i], camera, v))
let r = min(this.width, this.height) / 2 - 20
this.drawBasis(camera, r)
ctx.strokeStyle = ctx.fillStyle = color('dark')
ctx.beginPath()
edges.forEach(([a, b]) => {
let u = _points[a]
let v = _points[b]
ctx.moveTo(u.x* r, u.y * r)
ctx.lineTo(v.x* r, v.y * r)
})
ctx.stroke()
}})
</script>
```
Yes, but.
A first drawback is that computing vertices and edges is quite annoying.
Projecting every vertex on the screen for every redraw is also computationally
intensive. But most importantly, **it's ugly**. Indeed, edges are straight, and
you need a lot of them to convince yourself that you are looking at a sphere.
So much so that it clutters the canvas, and makes it very hard to legibly see
what is behind the sphere. Which I want to do, because it's supposed to be a
semi-transparent gizmo.
I am the bearer of *great news*: it is actually possible to draw a wireframe sphere
(really, any 3d-rotated circle) in Canvas2D, with:
- sexy **smooth** edges, like in the first example, thanks to the `ellipse()` function.
- no need to compute individual vertices and edges.
- a short implementation (I'd argue it's simpler than manually computing vertices)
## Sphere sections
The first thing to notice is that the wireframe above is only composed of
*circles*. And not just any circles, but circles that *lay on the surface of the
sphere*. They can therefore each be characterized as the intersection of a plane
and the sphere, i.e be described by a normal vector `n` (orthogonal to the plane)
and an offset `o` (`-1 < o < 1`) for the displacement of the plane along this
normal vector.
```{=html}
<figure>
<canvas id="circ" class="r1 hw"></canvas>
<input id="circ_offset" type="range" min="-1" step="any" max="1" value=".5">
</figure>
<script type="module">
let _V = vec()
let p1 = vec(), p2 = vec()
let normal = vec(0, 1 / sqrt(2), 1 / sqrt(2))
let camera = { theta: PI / 4, phi: .5 }
let _normal = project(vec(), camera, normal)
new Canvas('circ', {
inputs: [circ_offset],
render(ctx) {
ctx.clearRect(0, 0, this.width, this.height)
ctx.translate(this.width / 2, this.height / 2)
ctx.scale(1, -1)
ctx.lineCap = 'round'
let x = this.width / 4
let radius = min(this.width, this.height) / 2 / sqrt(2) - 20
this.drawBasis(camera, radius)
this.drawVec(camera, Z, radius, color('cyan'), 3)
project(_normal, camera, Z)
project(p1, camera, X)
project(p2, camera, Y)
this.drawCircle(0, 0, radius, color('dark'), 4)
this.drawWireframe(camera, radius)
ctx.save()
ctx.globalAlpha = .5
ctx.fillStyle = color('yellow')
ctx.scale(radius, radius)
ctx.translate(_normal.x * circ_offset.value, _normal.y * circ_offset.value)
ctx.beginPath()
ctx.moveTo(p1.x + p2.x, p1.y + p2.y)
ctx.lineTo(p1.x - p2.x, p1.y - p2.y)
ctx.lineTo(- p1.x - p2.x, - p1.y - p2.y)
ctx.lineTo(- p1.x + p2.x, - p1.y + p2.y)
ctx.fill()
ctx.restore()
this.pathSection(camera, Z, radius, circ_offset.value)
ctx.strokeStyle = color('yellow')
this.drawSectionFront(camera, Z, radius, circ_offset.value, color('yellow'), 3)
// ctx.save()
// ctx.beginPath()
// this.pathSection(camera, Z, radius, circ_offset.value)
// ctx.clip()
// ctx.strokeStyle = color('dark')
// ctx.lineWidth = 4
// this.drawWireframe(camera, radius)
// ctx.restore()
// ctx.lineWidth = 6
// ctx.strokeStyle = color('yellow')
// this.drawSection(this.camera, this.n, r, circ_o.value)
}
})
</script>
```
And indeed, to obtain the wireframe from earlier, we just have to cut the
sphere with:
- a plane rotating around the z axis ;
- a plane orthogonal to z, shifting along the z axis.
## Projected circles are ellipses
Now that we know what the wireframe is made of, and how to characterize these
intersections, how do we render them?
The key is to realize that the projection of a circle on the screen will *always*
be an ellipse. Conveniently, Canvas2D provides us with a way to draw ellipses,
defined as such:
> `ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle)`
>
> The `ellipse()` method creates an elliptical arc centered at `(x, y)` with the
> radii `radiusX` and `radiusY`. The path starts at `startAngle` and ends at
> `endAngle`.
We just need to find what the parameters should be for any given section.
Because we're only doing orthographic projection here, it turns out it's really
not difficult:
- the angle of the ellipse is the angle of the (projected) normal vector on screen.
- the radius of the circle is `(1 - o * o)`, which becomes the `radiusY` of the ellipse.
- the `radiusX` of the ellipse is the radius of the circle times how much of the (projected)
normal vector is facing us.
### Occlusion culling
We're already able to generate pretty, smooth wireframes. Now let's
take care of *occlusion culling*. That is, only draw the arc of the ellipse
that is directly facing us.
(Actually the MDN docs are wrong here, startAngle and endAngle are the
eccentric angles of the start and end points, not an actual measurable
geometric angle).
## Putting things in perspective
So far so good. However to actually convince anyone of the versatility of this
method, we really ought to also support perspective projection. Good news:
perspective-projected circles are *still* ellipses. Bad news: computing the
parameters of the ellipse is a tad more involved, as:
- the angle of the projected normal on the screen is no longer the angle of the
ellipse ;
- the perspective-projection of the center of the circle is no longer the
center of the ellipse.
I'm gonna show you my method --- but I really would like a simpler one, so if
you know better, please tell me!
Until them I purposefully getting specific about the coordinate system we're
using and camera projection. But to delve into perspective projection, we cannot
cut it.
---
### Equations
You can skip this if you just want to get to the final implementation.
Without loss of generality, we can just assume we have everything expressed in
the camera coordinate system (the projection used before gets you there).
Let's consider the points on screen (at depth `z=1`) that belong to the ellipse.
They must be at coordinate `\overrightarrow{p} = (s \cdot x, s \cdot y, s)` for some $s$ because that's how
projection scaling works. The coordinates of its projection is `\overrightarrow{q} = \overrightarrow{p} / s = (x, y, 1)`
Now, this point belongs to the original circle,
therefore it satisfies the following equations:
- they are on the plane orthogonal to normal `\overrightarrow{n}` and going through `\overrightarrow{c}`,
the center of the circle:
`(\overrightarrow{p} - \overrightarrow{c}) \cdot \overrightarrow{n} = 0`
- they are at distance `r` from the line directed by `\overrightarrow{n}` going through `\overrightarrow{c}`:
`\|(\overrightarrow{p} - \overrightarrow{c}) \times \overrightarrow{n}\| = r`
The first equation allows us to express $s$ in terms of $x$ and $y$:
`s = \frac{\overrightarrow{c} \cdot \overrightarrow{n}}{\overrightarrow{q} \cdot \overrightarrow{n}}`
Replacing $s$ in the second equations yields:
```
\begin{align*}&\|(\frac{\overrightarrow{c} \cdot \overrightarrow{n}}{\overrightarrow{q} \cdot
\overrightarrow{n}} \cdot \overrightarrow{q} - \overrightarrow{c}) \times
\overrightarrow{n}\| \\
=\ &\|\frac{\overrightarrow{c} \cdot \overrightarrow{n}}{\overrightarrow{q} \cdot
\overrightarrow{n}} \cdot \overrightarrow{q} \times \overrightarrow{n} - \overrightarrow{c} \times
\overrightarrow{n}\| \\
=\ &r^2\end{align*}
```
Squaring and expanding the second equation gives us:
```
$$\begin{align*}
&((x \cdot s - c_x) \cdot n_y - (y \cdot s - c_y) \cdot n_x)^2 \\
+\ &((s - c_z) \cdot n_x - (x \cdot s - c_x) \cdot n_z)^2 \\
+\ &((y \cdot s - c_y) \cdot n_z - (s - c_z) \cdot n_y)^2 = r^2
\end{align*}
$$
```
----
## In conclusion
I hope this little interactive article will be useful for someone, anyone. Did
I spend way too much time thinking about ellipses for such an insignificant
result? Yes. But it's such neat technique I couldn't keep it for myself.
```{=html}
<!-- TODO: pre-compile katex to MathML only -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.3/dist/katex.min.css" integrity="sha384-Juol1FqnotbkyZUT5Z7gUPjQ9gzlwCENvUZTpQBAPxtusdwFLRy382PSDx5UUJ4/" crossorigin="anonymous"> -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.3/dist/katex.min.js" integrity="sha384-97gW6UIJxnlKemYavrqDHSX3SiygeOwIZhwyOKRfSaf0JWKRVj9hLASHgFTzT+0O" crossorigin="anonymous"></script>
<script type="module">
const macros = {}
document.querySelectorAll('.math').forEach(elem => {
katex.render(elem.innerText, elem, {throwOnError: false, macros,
displayMode: !(elem.classList.contains('inline')), output:'mathml'})
})
</script>
```

0
content/projects.rst Normal file → Executable file
View File

4
content/projects/achille/index.markdown Normal file → Executable file
View File

@ -10,5 +10,9 @@ labels:
**achille** [aʃil] is a tiny Haskell library for building your very own **static site
generator**. It is in spirit a direct successor to [Hakyll][Hakyll].
**achille** is currently undergoing a *full rewrite*, that you can keep track of
on [github](https://github.com/flupe/achille). I just figured out the last missing
bits needed to make it *truly* easy to use, and way more powerful. Stay tuned!
[Hakyll]: https://jaspervdj.be/hakyll

5
content/projects/achille/logo.svg Normal file → Executable file
View File

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1" height="20px" width="20px">
<path d="M 18,2 A 5,5 0 0 0 13,7 L 13, 13 A 5, 5 0 0 0 18,17 M 13,7 A 5,5 0 0 0 8,2 L 7,2 A 5,5 0 0 0 2,7 L 2,12 A 5, 5 0 0 0 7,17 L 10,17" style="stroke-width: 1; stroke-linecap: round; stroke-linejoin: bevel; stroke: #4c566a" fill="none"></path>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path d="M 18,2 A 5,5 0 0 0 13,7 L 13, 13 A 5, 5 0 0 0 18,17 M 13,7 A 5,5 0 0 0 8,2 L 7,2 A 5,5 0 0 0 2,7 L 2,12 A 5, 5 0 0 0 7,17 L 10,17"></path>
</svg>

Before

Width:  |  Height:  |  Size: 414 B

After

Width:  |  Height:  |  Size: 232 B

7
content/projects/achille/pages/1-motivation.markdown Normal file → Executable file
View File

@ -2,6 +2,13 @@
title: Motivation
---
> **achille** is currently undergoing a *full rewrite*, that you can keep track of
> on [github](https://github.com/flupe/achille). I just figured out the last missing
> bits needed to make it *truly* easy to use, and way more powerful. Stay tuned!
>
> The following page is largely outdated, as the syntax and internals *will change*.
> `Recipe m a` is *no longer a monad*, and this is *crucial*.
## Motivation
Static site generators (SSG) have proven to be very useful tools for easily

8
content/projects/achille/pages/2-how-it-works.markdown Normal file → Executable file
View File

@ -2,6 +2,14 @@
title: How achille works
---
> **achille** is currently undergoing a *full rewrite*, that you can keep track of
> on [github](https://github.com/flupe/achille). I just figured out the last missing
> bits needed to make it *truly* easy to use, and way more powerful. Stay tuned!
>
> The following page is largely outdated, as the syntax and internals *will change*.
> `Recipe m a` is *no longer a monad*, and this is *crucial*.
> You can safely ignore anything on this page.
### Caching
So far we haven't talked about caching and incremental builds.

View File

@ -2,6 +2,12 @@
title: Making a blog from scratch
---
> **achille** is currently undergoing a *full rewrite*, that you can keep track of
> on [github](https://github.com/flupe/achille). I just figured out the last missing
> bits needed to make it *truly* easy to use, and way more powerful. Stay tuned!
>
> The following page is largely outdated, as the syntax and internals *will change*.
# Making a blog from scratch
In this tutorial we'll see how to use **achille** for a simple blog generator.

6
content/projects/achille/pages/4-examples.markdown Normal file → Executable file
View File

@ -5,9 +5,9 @@ title: Examples
## Achille in the wild
**achille** is very new, obscure and ill-documented.
Therefore there are currently few concrete examples to show how people use it.
I pretty much am the only user.
Therefore there are currently few concrete examples demonstrating how people
use it. I pretty much am the only user. Oh well.
- [acatalepsie.fr](https://acatalepsie.fr) ([source](https://github.com/flupe/site))
- [acatalepsie.fr](https://acatalepsie.fr)
- [sbi.re](https://sbi.re)
- [lucas.escot.me](https://lucas.escot.me)

0
content/projects/jibniz/index.markdown Normal file → Executable file
View File

6
content/projects/jibniz/logo.svg Normal file → Executable file
View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="-10 -10 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1" height="20px" width="20px">
<!-- <path d="M 18,2 A 5,5 0 0 0 13,7 L 13, 13 A 5, 5 0 0 0 18,17 M 13,7 A 5,5 0 0 0 8,2 L 7,2 A 5,5 0 0 0 2,7 L 2,12 A 5, 5 0 0 0 7,17 L 10,17" style="stroke-width: 1; stroke-linecap: round; stroke-linejoin: bevel; stroke: #4c566a" fill="none"></path>-->
<rect stroke="#4c566a" fill="none" id="r1" width="16" height="16" x="-8" y="-8"/>
<svg viewBox="-10 -10 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect id="r1" width="16" height="16" x="-8" y="-8"/>
<use id="r2" href="#r1" transform="rotate(-20 0 0) scale(.78)"/>
<use id="r3" href="#r2" transform="rotate(-20 0 0) scale(.78)"/>
<use id="r4" href="#r3" transform="rotate(-20 0 0) scale(.78)"/>

Before

Width:  |  Height:  |  Size: 912 B

After

Width:  |  Height:  |  Size: 543 B

9
content/projects/kagu/index.md Executable file
View File

@ -0,0 +1,9 @@
---
title: kagu
subtitle: A toy dependently-typed language
year: "2021"
labels: {}
---
Nothing to see here (yet) (or ever).

3
content/projects/kagu/logo.svg Executable file
View File

@ -0,0 +1,3 @@
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path d=" M 2 2 L 2 18 M 2 10 L 14 10 A 5,5 0 0 0 18,5 L 18 2 M 14 10 A 5,5 0 0 1 18,15 L 18 18"></path>
</svg>

After

Width:  |  Height:  |  Size: 189 B

27
content/projects/slod/index.md Executable file
View File

@ -0,0 +1,27 @@
---
title: slod
subtitle: A tiny HTTP server for static files with live reloading
year: "2021"
labels:
repo: flupe/slod
license: GPLv3
---
> Warning: this is poorly implemented, and also doesn't have live-reloading yet
> Somehow I still use it daily, but you shouldn't, at all cost.
I grew tired of having to use `python -m http.server` every time I wanted to
serve static files locally --- that is, quite often --- so I implemented my own
tiny HTTP server in C.
- No dependencies
- Tiny
- Small
- Have I said it's tiny?
```raw
Usage: slod [options] [ROOT]
-h, --help Show this help message and quit.
-p, --port PORT Specify port to listen on.
-l, --live Enable livereload.
```

4
content/projects/slod/logo.svg Executable file
View File

@ -0,0 +1,4 @@
<svg viewBox="-10 -10 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1">
<rect width="12" height="12" x="-8" y="-8"/>
<rect width="12" height="12" x="-4" y="-4"/>
</svg>

After

Width:  |  Height:  |  Size: 180 B

0
content/projects/troid/index.rst Normal file → Executable file
View File

7
content/projects/troid/logo.svg Normal file → Executable file
View File

@ -1,6 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1" height="20px" width="20px">
<!-- <path d="M 18,2 A 5,5 0 0 0 13,7 L 13, 13 A 5, 5 0 0 0 18,17 M 13,7 A 5,5 0 0 0 8,2 L 7,2 A 5,5 0 0 0 2,7 L 2,12 A 5, 5 0 0 0 7,17 L 10,17" style="stroke-width: 1; stroke-linecap: round; stroke-linejoin: bevel; stroke: #4c566a" fill="none"></path>-->
<path d="M 2,18 L 10,8 L 18,18 Z M 2,12 L 10,2 L 18,12 Z
M 2,18 L 2,12 M 10,8 L 10,2 M 18,18 L 18, 12 " style="stroke-width: 1; stroke-linecap: round; stroke-linejoin: bevel; stroke: #4c566a" fill="none"></path>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path d="M 2,18 L 10,8 L 18,18 Z M 2,12 L 10,2 L 18,12 Z M 2,18 L 2,12 M 10,8 L 10,2 M 18,18 L 18, 12 "></path>
</svg>

Before

Width:  |  Height:  |  Size: 647 B

After

Width:  |  Height:  |  Size: 196 B

10
content/quid.rst Normal file → Executable file
View File

@ -1,4 +1,6 @@
This site is under construction, please be kind.
If you want to lift the veil of pseudonymity, feel free to visit my
`professional page <https://lucas.escot.me>`_.
-----
@ -7,8 +9,10 @@ otherwise. In other words, you are free to copy, redistribute and edit this
content, provided you: give appropriate credit; indicate where changes were made
and do not do so for commercial purposes.
This website is self-hosted on a server somewhere in Lyon, France.
The domain name `acatalepsie.fr <https://acatalepsie.fr>`_ has
been registered at `gandi.net <https://gandi.net>`_.
This website is self-hosted on a home server somewhere in Lyon, France.
The domain name `acatalepsie.fr <https://acatalepsie.fr>`_ has been registered
at `gandi.net <https://gandi.net>`_. It's not a beefy machine, meaning the site
may go down if there's too much traffic. That's life, you're trying to reach a
specific computer across the globe after all.
.. _CC BY-NC 2.0: https://creativecommons.org/licenses/by-nc/2.0/

0
content/readings.yaml Normal file → Executable file
View File

0
content/visual.rst Normal file → Executable file
View File

3
deploy.sh Executable file
View File

@ -0,0 +1,3 @@
curl --oauth2-bearer "$(cat SRHT_TOKEN)" \
-Fcontent=@site.tar.gz \
https://pages.sr.ht/publish/flupe.srht.site

View File

@ -60,8 +60,9 @@ buildPost src = do
pandoc' <- processMath pandoc
content <- renderPandocWith wopts pandoc'
let time = timestamp (unpack date)
pure (renderPost time title content)
>>= write (src -<.> "html")
rendered <- pure (renderPost time title content)
write (dropExtension src </> "index.html") rendered
write (src -<.> "html") rendered
<&> Post title time (fromMaybe False draft) Nothing content
where

View File

@ -44,7 +44,8 @@ buildProject src = do
<&> renderProject meta icon children
>>= write (src -<.> "html")
(meta, icon,) <$> getCurrentDir
pure (meta, icon, "/projects" </> name <> "/")
where
buildChildren :: String -> Task IO [(Text, FilePath)]
buildChildren name = match "pages/*" \filepath -> do