Compare commits
19
Commits
@@ -18,7 +18,7 @@ on:
|
|||||||
- .gitea/workflows/deploy.yml
|
- .gitea/workflows/deploy.yml
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PORTAL_RELEASE: v0.3.0
|
PORTAL_RELEASE: v0.3.6
|
||||||
INSTANCE: redoal-portal
|
INSTANCE: redoal-portal
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -67,6 +67,10 @@ jobs:
|
|||||||
# The release tarball carries the site bundle at site/ (no
|
# The release tarball carries the site bundle at site/ (no
|
||||||
# target/ prefix), so override Cargo.toml's build-time path.
|
# target/ prefix), so override Cargo.toml's build-time path.
|
||||||
LEPTOS_SITE_ROOT=site
|
LEPTOS_SITE_ROOT=site
|
||||||
|
# Portal ships content-hashed pkg files (portal.<hash>.js) with
|
||||||
|
# a hash.txt in the site root; this makes the server reference
|
||||||
|
# them, so a stale cached bundle can never pair with new wasm.
|
||||||
|
LEPTOS_HASH_FILES=true
|
||||||
# Read-only Gitea token - here it also backs the
|
# Read-only Gitea token - here it also backs the
|
||||||
# gitea_releases source for the private redoal/redoal repo,
|
# gitea_releases source for the private redoal/redoal repo,
|
||||||
# so the owning user needs read access there.
|
# so the owning user needs read access there.
|
||||||
@@ -79,9 +83,16 @@ jobs:
|
|||||||
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
|
# No sudo: the runner's unit sets NoNewPrivileges=yes - systemctl
|
||||||
# talks to PID1 over D-Bus, authorized by the polkit rule scoped
|
# talks to PID1 over D-Bus, authorized by the polkit rule scoped
|
||||||
# to deploy-runner + the app@* unit pattern.
|
# to deploy-runner + the app@* unit pattern.
|
||||||
|
# enable: the unit must come back after a host reboot (2026-08-30 a
|
||||||
|
# reboot left both portal instances down - deploys had only ever
|
||||||
|
# started them). Needs the manage-unit-files polkit grant; until
|
||||||
|
# that's on the host the enable is reported and skipped, never a
|
||||||
|
# failed deploy.
|
||||||
- name: Activate and restart
|
- name: Activate and restart
|
||||||
run: |
|
run: |
|
||||||
ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current
|
ln -sfn "/srv/app/$INSTANCE/releases/$PORTAL_RELEASE" /srv/app/$INSTANCE/current
|
||||||
|
systemctl enable app@$INSTANCE.service \
|
||||||
|
|| echo "::warning::could not enable app@$INSTANCE (polkit) - unit will not survive a reboot"
|
||||||
systemctl restart app@$INSTANCE.service
|
systemctl restart app@$INSTANCE.service
|
||||||
|
|
||||||
# www redirects to the apex for the same single-cookie-scope
|
# www redirects to the apex for the same single-cookie-scope
|
||||||
|
|||||||
@@ -72,48 +72,96 @@ export function turningFunctionToPoints(turning, samples = 14) {
|
|||||||
return pts;
|
return pts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A path plus the motion that is its own: where it sits, how it
|
||||||
|
// spins, how it drifts, how fast it traces itself in. Every rate is
|
||||||
|
// an integer number of cycles so the field loops seamlessly at t = 1.
|
||||||
export function makePaths(seed, count, stops = 10) {
|
export function makePaths(seed, count, stops = 10) {
|
||||||
const rand = mulberry32(seed);
|
const rand = mulberry32(seed);
|
||||||
const paths = [];
|
const paths = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
paths.push(turningFunctionToPoints(createTurningFunction(rand, stops)));
|
const pts = turningFunctionToPoints(createTurningFunction(rand, stops));
|
||||||
|
let cx = 0, cy = 0;
|
||||||
|
for (let k = 0; k < pts.length; k += 2) { cx += pts[k]; cy += pts[k + 1]; }
|
||||||
|
cx /= pts.length / 2;
|
||||||
|
cy /= pts.length / 2;
|
||||||
|
const sign = rand() > 0.5 ? 1 : -1;
|
||||||
|
paths.push({
|
||||||
|
pts,
|
||||||
|
cx,
|
||||||
|
cy,
|
||||||
|
// own turns per cycle: a third sit still, the rest turn one
|
||||||
|
// or two full turns either way over the cycle
|
||||||
|
spin: rand() < 0.33 ? 0 : sign * (1 + Math.floor(rand() * 2)),
|
||||||
|
// slow orbit around its own place, radius in frame units
|
||||||
|
driftR: 0.01 + rand() * 0.03,
|
||||||
|
driftK: 1 + Math.floor(rand() * 3),
|
||||||
|
driftPhase: rand() * Math.PI * 2,
|
||||||
|
// its own wave along the line
|
||||||
|
waveF: 4 + rand() * 6,
|
||||||
|
waveK: 1 + Math.floor(rand() * 2),
|
||||||
|
waveA: 0.004 + rand() * 0.008,
|
||||||
|
// tracing tempo: how many times it redraws itself per cycle
|
||||||
|
tempo: 1 + Math.floor(rand() * 2),
|
||||||
|
phase: rand(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
// t in [0, 1) is the animation phase. Each path traces itself in over
|
// t in [0, 1) is the animation phase. Each path traces itself in at
|
||||||
// one cycle, offset by its index so the field is always mid-draw, and
|
// its own tempo and phase, fades as it completes, turns at its own
|
||||||
// fades as it completes; the whole field drifts slowly and a soft
|
// rate about its own center and drifts on its own small orbit; a wave
|
||||||
// wave runs through every line. `colors` is [accent, dim] - the
|
// with its own frequency runs along it. The tip is interpolated
|
||||||
// palette comes from the page's own custom properties either way.
|
// between samples so a trace glides instead of stepping. `colors` is
|
||||||
|
// [accent, dim] - the palette comes from the page's custom properties.
|
||||||
|
const smooth = (u) => u * u * (3 - 2 * u);
|
||||||
|
|
||||||
export function drawPaths(ctx, width, height, paths, t, colors) {
|
export function drawPaths(ctx, width, height, paths, t, colors) {
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
const s = Math.min(width, height) * 0.9;
|
const s = Math.min(width, height) * 0.9;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.translate(width / 2, height / 2);
|
ctx.translate(width / 2, height / 2);
|
||||||
ctx.rotate(t * Math.PI * 2 * 0.08);
|
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.lineCap = 'round';
|
ctx.lineCap = 'round';
|
||||||
ctx.lineJoin = 'round';
|
ctx.lineJoin = 'round';
|
||||||
const n = paths.length;
|
const TAU = Math.PI * 2;
|
||||||
const wave = t * Math.PI * 2;
|
for (let i = 0; i < paths.length; i++) {
|
||||||
for (let i = 0; i < n; i++) {
|
const p = paths[i];
|
||||||
const pts = paths[i];
|
const phase = (t * p.tempo + p.phase) % 1;
|
||||||
const phase = (t + i / n) % 1;
|
// ease: quick to appear, long to complete, then fade
|
||||||
// ease: quick to appear, long to complete
|
const progress = smooth(Math.min(1, phase * 1.35));
|
||||||
const progress = Math.min(1, phase * 1.35);
|
|
||||||
const count = Math.max(2, Math.floor((pts.length / 2) * progress));
|
|
||||||
const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15);
|
const alpha = phase < 0.85 ? 0.75 : 0.75 * (1 - (phase - 0.85) / 0.15);
|
||||||
|
const total = p.pts.length / 2;
|
||||||
|
const tip = 1 + (total - 1) * progress; // fractional sample index
|
||||||
|
const whole = Math.floor(tip);
|
||||||
|
const frac = tip - whole;
|
||||||
|
|
||||||
|
const angle = p.spin * TAU * t;
|
||||||
|
const dx = Math.cos(p.driftPhase + p.driftK * TAU * t) * p.driftR;
|
||||||
|
const dy = Math.sin(p.driftPhase + p.driftK * TAU * t) * p.driftR;
|
||||||
|
const wave = p.waveK * TAU * t;
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate((p.cx + dx) * s, (p.cy + dy) * s);
|
||||||
|
ctx.rotate(angle);
|
||||||
ctx.strokeStyle = i % 3 === 0 ? colors[0] : colors[1];
|
ctx.strokeStyle = i % 3 === 0 ? colors[0] : colors[1];
|
||||||
ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
|
ctx.globalAlpha = alpha * (i % 3 === 0 ? 1 : 0.55);
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let k = 0; k < count; k++) {
|
const at = (k) => {
|
||||||
const x = pts[k * 2], y = pts[k * 2 + 1];
|
const x = p.pts[k * 2] - p.cx, y = p.pts[k * 2 + 1] - p.cy;
|
||||||
const wy = y + Math.sin(x * 7 + wave + i) * 0.008;
|
return [x, y + Math.sin(x * p.waveF + wave) * p.waveA];
|
||||||
if (k === 0) ctx.moveTo(x * s, wy * s);
|
};
|
||||||
else ctx.lineTo(x * s, wy * s);
|
for (let k = 0; k <= Math.min(whole, total - 1); k++) {
|
||||||
|
const [x, y] = at(k);
|
||||||
|
if (k === 0) ctx.moveTo(x * s, y * s);
|
||||||
|
else ctx.lineTo(x * s, y * s);
|
||||||
|
}
|
||||||
|
if (whole < total - 1 && frac > 0) {
|
||||||
|
const [ax, ay] = at(whole), [bx, by] = at(whole + 1);
|
||||||
|
ctx.lineTo((ax + (bx - ax) * frac) * s, (ay + (by - ay) * frac) * s);
|
||||||
}
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# The group exhibition at Podium, connected to the conference: opens
|
||||||
|
# 21 October 2026 and runs for a month. Announced in every header
|
||||||
|
# while it runs (portal's event: window); afterwards a followup, and
|
||||||
|
# a "post what happened" task lands on /review.
|
||||||
|
name: Where can you see it in person?
|
||||||
|
description: >
|
||||||
|
redoal is part of the group exhibition at Podium, opening 21 October
|
||||||
|
and running for a month alongside the conference. Draw on the
|
||||||
|
screens, leave a voice at a shape, hear who came before you.
|
||||||
|
event:
|
||||||
|
starts: 2026-10-21T18:00:00+02:00
|
||||||
|
duration: 31d
|
||||||
|
place: Podium, Oslo
|
||||||
|
alternatives:
|
||||||
|
- name: Come draw on the wall
|
||||||
|
description: >
|
||||||
|
The installation runs the same code as this site, on screens
|
||||||
|
you can stand in front of together — the network at the scale
|
||||||
|
of a room.
|
||||||
|
features:
|
||||||
|
- name: Opening 21 October
|
||||||
|
description: >
|
||||||
|
The exhibition opens on the 21st and stays up for a month,
|
||||||
|
within the conference programme. Come to the opening, or
|
||||||
|
any day after.
|
||||||
|
icon: lucide:calendar-days
|
||||||
|
- name: Draw, then listen
|
||||||
|
description: >
|
||||||
|
Draw a shape on the touch screen and arrive at a place.
|
||||||
|
Listen to the voices left there, add your own, and watch
|
||||||
|
your line find the lines drawn before it.
|
||||||
|
icon: lucide:hand
|
||||||
|
- name: Take the zine with you
|
||||||
|
description: >
|
||||||
|
The printed zine sits in the room — six short pieces on what
|
||||||
|
a network like this could mean. Fold one, hand it on.
|
||||||
|
icon: lucide:book-open
|
||||||
+60
-3
@@ -1,9 +1,10 @@
|
|||||||
id: /
|
id: /
|
||||||
name: What shapes us?
|
name: What shapes us?
|
||||||
description: >
|
description: >
|
||||||
redoal is a gesture-addressed network — peers find each other by
|
redoal is a gesture-addressed network. Draw a line and it becomes
|
||||||
drawing similar shapes. The gesture is the address. Draw one below
|
an address — the drawing itself, ready to use the moment your hand
|
||||||
and see who's near.
|
lifts. Similar drawings land near each other, and everything else
|
||||||
|
follows from that. Draw one below and see who's near.
|
||||||
responsible:
|
responsible:
|
||||||
name: Bendik Aagaard Lynghaug
|
name: Bendik Aagaard Lynghaug
|
||||||
contact: bl@uhhm.no
|
contact: bl@uhhm.no
|
||||||
@@ -14,6 +15,9 @@ alternatives:
|
|||||||
discover signal within chaos. Draw a path — the network answers
|
discover signal within chaos. Draw a path — the network answers
|
||||||
with the shapes it heard nearby.
|
with the shapes it heard nearby.
|
||||||
action: /shape
|
action: /shape
|
||||||
|
# Announced, not yet open: the network answers with echoes today,
|
||||||
|
# what follows a shape is still being built.
|
||||||
|
disabled: true
|
||||||
consequence: [Find what follows]
|
consequence: [Find what follows]
|
||||||
record_as: redoal_gestures
|
record_as: redoal_gestures
|
||||||
encouragements:
|
encouragements:
|
||||||
@@ -37,6 +41,58 @@ alternatives:
|
|||||||
type: email
|
type: email
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
- name: Meet a network you carry in your hand
|
||||||
|
description: >
|
||||||
|
The one trick, and what it enables — the parts that run today,
|
||||||
|
said plainly.
|
||||||
|
features:
|
||||||
|
- name: A drawing is an address
|
||||||
|
description: >
|
||||||
|
The computer measures how your line curves, turns, and keeps
|
||||||
|
rhythm, and boils it down to a twenty-byte fingerprint.
|
||||||
|
Similar drawings get similar fingerprints — two spirals land
|
||||||
|
like two houses on the same street, and that closeness is
|
||||||
|
the whole network.
|
||||||
|
icon: lucide:map-pin
|
||||||
|
- name: Your machine asks the room
|
||||||
|
description: >
|
||||||
|
To find someone, your machine asks the machines around it:
|
||||||
|
"has anyone been near this shape?" Your spiral finds my
|
||||||
|
spiral directly, peer to peer. The whole directory is the
|
||||||
|
shapes people know, passed along on a napkin.
|
||||||
|
icon: lucide:users
|
||||||
|
- name: Voices stay where you left them
|
||||||
|
description: >
|
||||||
|
A drawing is a place, and places hold voices. Listen to the
|
||||||
|
recordings people left at a gesture, leave your own on top,
|
||||||
|
and when someone stands at the same drawing right now, just
|
||||||
|
talk — live, machine to machine.
|
||||||
|
icon: lucide:mic
|
||||||
|
- name: Sealed by your own hand
|
||||||
|
description: >
|
||||||
|
Every recording and every stored stroke carries a signature
|
||||||
|
from the leaver's own machine — proof that it arrived
|
||||||
|
intact and that the same hand made the same things. The key
|
||||||
|
lives only with you: keep it and you're recognized, delete
|
||||||
|
it and you were never here.
|
||||||
|
icon: lucide:stamp
|
||||||
|
- name: Draw and speak, and you're in
|
||||||
|
description: >
|
||||||
|
The primitives are the drawn line and the human voice. A
|
||||||
|
child, an elder, or a newcomer still learning the local
|
||||||
|
script arrives on the same terms as everyone else — that is
|
||||||
|
the whole entry requirement.
|
||||||
|
icon: lucide:hand
|
||||||
|
- name: Young, and honest about it
|
||||||
|
description: >
|
||||||
|
A prototype and an artwork before it is a product. The
|
||||||
|
gesture math, the fingerprints, the voice recordings, and the
|
||||||
|
machine-to-machine fetching are real and running; the
|
||||||
|
transport that reaches across the internet is being built
|
||||||
|
now. The edges are rough, and the edges are where the
|
||||||
|
interesting questions live.
|
||||||
|
icon: lucide:sprout
|
||||||
|
|
||||||
- name: Follow the build as it lands
|
- name: Follow the build as it lands
|
||||||
description: >
|
description: >
|
||||||
The network as it is being built — and we need collaborators on
|
The network as it is being built — and we need collaborators on
|
||||||
@@ -57,6 +113,7 @@ alternatives:
|
|||||||
owner: redoal
|
owner: redoal
|
||||||
repo: redoal
|
repo: redoal
|
||||||
public: true
|
public: true
|
||||||
|
empty: Nothing released yet — the first cut lands here.
|
||||||
# url pinned to null while the repo is private - a private
|
# url pinned to null while the repo is private - a private
|
||||||
# release's html_url 404s for anonymous visitors, and the
|
# release's html_url 404s for anonymous visitors, and the
|
||||||
# card renders an unlinked heading instead (same lesson as
|
# card renders an unlinked heading instead (same lesson as
|
||||||
|
|||||||
@@ -37,3 +37,26 @@ alternatives:
|
|||||||
transitions:
|
transitions:
|
||||||
- to: unsubscribed
|
- to: unsubscribed
|
||||||
label: Unsubscribe
|
label: Unsubscribe
|
||||||
|
|
||||||
|
- name: Announced pages
|
||||||
|
description: >
|
||||||
|
Pages with an `event:` window, kept here by portal itself. When a
|
||||||
|
window closes the record becomes a task: post what happened,
|
||||||
|
then mark it done.
|
||||||
|
action: /review
|
||||||
|
consequence: [Confirm]
|
||||||
|
features:
|
||||||
|
- name: ""
|
||||||
|
description: ""
|
||||||
|
resource:
|
||||||
|
source:
|
||||||
|
kind: kv
|
||||||
|
bucket: portal_events
|
||||||
|
requires_group: owners
|
||||||
|
transitions:
|
||||||
|
- from: awaiting_summary
|
||||||
|
to: summarized
|
||||||
|
label: Posted what happened
|
||||||
|
- from: announced
|
||||||
|
to: summarized
|
||||||
|
label: Needs no summary
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ alternatives:
|
|||||||
description: >
|
description: >
|
||||||
A drawn path is a fact, not an account. There is no profile
|
A drawn path is a fact, not an account. There is no profile
|
||||||
behind it unless you left an email.
|
behind it unless you left an email.
|
||||||
|
- name: What arrives here next
|
||||||
|
description: >
|
||||||
|
In the full network a drawing is a place that holds voices —
|
||||||
|
recordings left by whoever came before, and live talk with
|
||||||
|
whoever is standing here now. On this site it holds echoes
|
||||||
|
today; the voices follow.
|
||||||
|
|
||||||
- name: Draw another
|
- name: Draw another
|
||||||
description: >
|
description: >
|
||||||
|
|||||||
Reference in New Issue
Block a user