Compare commits
2 Commits
6dcb10c68b
...
2a4fffbb2a
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a4fffbb2a | |||
| 3f2d25f90e |
55
fable/Proof1/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Proof1 — the lit chamber
|
||||
|
||||
*First physical translation: screen → acrylic → light. Based on
|
||||
`REFINED_Backlit7/merge-45 → merge-55`.*
|
||||
|
||||
## What this is
|
||||
|
||||
A bubble chamber photographed the invisible: a particle passed, boiled a
|
||||
line of bubbles into being, and was gone — leaving a trail as its only
|
||||
confession. This project makes new such confessions by hand-parameter, then
|
||||
stages one as a world. *One Sun Setting*: a single event erupting at the
|
||||
waterline of a quiet sea, beneath a sun already fading into memory. Silence
|
||||
and violence in one frame.
|
||||
|
||||
## Inspiration
|
||||
|
||||
CERN plate film on a light table. Hokusai's small figures under enormous
|
||||
weather. Twombly's one honest gesture on a still ground. The idea that
|
||||
*evidence of the invisible rhymes across scales* — the spiral of a δ-ray and
|
||||
the spiral of a milkweed seed are the same handwriting.
|
||||
|
||||
## Boundaries
|
||||
|
||||
The archive costume came off, layer by layer, until only the gesture
|
||||
remained. No scanner's hand. No labels — a legible glyph commands *read me*;
|
||||
this piece asks only *look*. The palette holds: β-law magenta, hot at birth,
|
||||
violet only as a last word. The sea stays calm so the event can be loud —
|
||||
loudness lives in a small mark against vastness, never in clutter. The sun
|
||||
recedes; it is what the light *was*, not a character. Composition obeys φ:
|
||||
the golden rectangle, the horizon on the golden division, sun and event vertex
|
||||
on opposing golden nodes.
|
||||
|
||||
## Craft
|
||||
|
||||
The seed is the whole fingerprint — an image and its four-metre master are one
|
||||
truth at two sizes. Bubbles are hollow: pale core, refracting rim, delicate as
|
||||
mist at nose distance. Where they crowd, they *merge* — one flat union at an
|
||||
even density, so a dense knot never becomes mud, and, lit, never punches a dark
|
||||
hole in the glow. Even ink = even light. That is the whole discipline of the
|
||||
backlit grammar: **depth is transparency.** The deepest plane carries the least
|
||||
ink and the most light.
|
||||
|
||||
## Goals
|
||||
|
||||
Take the locked frame apart into planes and give it air. Sun to the back, pure
|
||||
transmitted colour, a light source rather than a picture. Sea in the middle,
|
||||
a woven field. The event to the front, erupting toward the viewer. Air gaps
|
||||
turn faked blur into real parallax; the LED panel behind frosted acrylic turns
|
||||
faded ink into a sun that actually shines. Prove one hero, then let the typology
|
||||
grow around it — a wall of catalogued dawns over one vacuum sea.
|
||||
|
||||
## Feels
|
||||
|
||||
Arrival and dispersal at once. A single instant, embalmed, outliving the
|
||||
conditions that made it. Awe, not spectacle. The held breath before the wave.
|
||||
24
fable/Proof1/base/merge-45.svg
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
24
fable/Proof1/base/merge-55.svg
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
83
fable/Proof1/manifest.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"piece": "Proof1 — the lit chamber",
|
||||
"source": "REFINED_Backlit7 merge (mirror event, uniform-lace)",
|
||||
"seed": "MESON-5113#03",
|
||||
"bubbleOpacity": 0.5,
|
||||
"art": {
|
||||
"w": 1001,
|
||||
"h": 1620,
|
||||
"ratio": "phi (golden portrait)"
|
||||
},
|
||||
"sheet": {
|
||||
"w_mm": 330,
|
||||
"h_mm": 420,
|
||||
"note": "A3 — golden art square floated, clear margins = lit void"
|
||||
},
|
||||
"gaps_mm": "plane z is nominal; two scales: ~3mm within a sheet, ~9mm between",
|
||||
"planes": [
|
||||
{
|
||||
"id": "sun",
|
||||
"file": "plane-1-sun.svg",
|
||||
"z": 0,
|
||||
"blur": 1.4,
|
||||
"dim": 0.95,
|
||||
"white": "none",
|
||||
"glow": 1
|
||||
},
|
||||
{
|
||||
"id": "sea-far",
|
||||
"file": "plane-2-sea-far.svg",
|
||||
"z": 9,
|
||||
"blur": 0.9,
|
||||
"dim": 1,
|
||||
"white": "none",
|
||||
"glow": 0.7
|
||||
},
|
||||
{
|
||||
"id": "sea-mid",
|
||||
"file": "plane-3-sea-mid.svg",
|
||||
"z": 15,
|
||||
"blur": 0.5,
|
||||
"dim": 1,
|
||||
"white": "trace",
|
||||
"glow": 0.7
|
||||
},
|
||||
{
|
||||
"id": "sea-near",
|
||||
"file": "plane-4-sea-near.svg",
|
||||
"z": 18,
|
||||
"blur": 0.2,
|
||||
"dim": 1,
|
||||
"white": "trace",
|
||||
"glow": 0.7
|
||||
},
|
||||
{
|
||||
"id": "event-halo",
|
||||
"file": "plane-5-event-halo.svg",
|
||||
"z": 27,
|
||||
"blur": 2.4,
|
||||
"dim": 0.85,
|
||||
"white": "none",
|
||||
"glow": 0.9
|
||||
},
|
||||
{
|
||||
"id": "event-core",
|
||||
"file": "plane-6-event-core.svg",
|
||||
"z": 30,
|
||||
"blur": 0,
|
||||
"dim": 1,
|
||||
"white": "under",
|
||||
"glow": 0.5
|
||||
},
|
||||
{
|
||||
"id": "front-glass",
|
||||
"file": "plane-7-front-glass.svg",
|
||||
"z": 30,
|
||||
"blur": 0,
|
||||
"dim": 1,
|
||||
"white": "none",
|
||||
"glow": 0,
|
||||
"blend": "multiply"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
fable/Proof1/plane-1-sun.svg
Normal file
|
After Width: | Height: | Size: 206 KiB |
1
fable/Proof1/plane-2-sea-far.svg
Normal file
|
After Width: | Height: | Size: 396 KiB |
1
fable/Proof1/plane-3-sea-mid.svg
Normal file
|
After Width: | Height: | Size: 396 KiB |
1
fable/Proof1/plane-4-sea-near.svg
Normal file
|
After Width: | Height: | Size: 396 KiB |
1
fable/Proof1/plane-5-event-halo.svg
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
1
fable/Proof1/plane-6-event-core.svg
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
1
fable/Proof1/plane-7-front-glass.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1001" height="1620" viewBox="0 0 1001 1620"><image x="0" y="0" width="1001" height="1620" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAxIiBoZWlnaHQ9IjE2MjAiIHZpZXdCb3g9IjAgMCAxMDAxIDE2MjAiPgo8ZGVmcz4KIDxmaWx0ZXIgaWQ9ImR1c3QiIHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxmZVR1cmJ1bGVuY2UgdHlwZT0idHVyYnVsZW5jZSIgYmFzZUZyZXF1ZW5jeT0iMC41IiBudW1PY3RhdmVzPSIyIiBzZWVkPSIxMyIgc3RpdGNoVGlsZXM9InN0aXRjaCIgcmVzdWx0PSJuIi8+PGZlQ29sb3JNYXRyaXggaW49Im4iIHR5cGU9Im1hdHJpeCIgdmFsdWVzPSIwIDAgMCAwIDAuMjM1IDAgMCAwIDAgMC4xODggMCAwIDAgMCAwLjEzMyAwIDAgMCAwLjMwMCAtMC4xOCIvPjwvZmlsdGVyPgogPGZpbHRlciBpZD0iZm94IiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC4wMDQiIG51bU9jdGF2ZXM9IjMiIHNlZWQ9IjE3IiBzdGl0Y2hUaWxlcz0ic3RpdGNoIiByZXN1bHQ9Im4iLz48ZmVDb2xvck1hdHJpeCBpbj0ibiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMC40MiAwIDAgMCAwIDAuMzAgMCAwIDAgMCAwLjE2IDAgMCAwIDAuMTk4IC0wLjEyIi8+PC9maWx0ZXI+CjwvZGVmcz4KPHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2ZveCkiLz4KPHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2R1c3QpIi8+CjxsaW5lIHgxPSI0MTEiIHkxPSIzNDQiIHgyPSI0MzAiIHkyPSIxMjQ3IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA5MCIgc3Ryb2tlLXdpZHRoPSIwLjc5Ii8+PGxpbmUgeDE9IjcwMiIgeTE9IjY4IiB4Mj0iNjg3IiB5Mj0iMTQzNyIgc3Ryb2tlPSJyZ2IoNjAsNDgsMzQpIiBzdHJva2Utb3BhY2l0eT0iMC4wNTEiIHN0cm9rZS13aWR0aD0iMC43NyIvPjxsaW5lIHgxPSI0MjIiIHkxPSI1MTQiIHgyPSI0MzciIHkyPSIxNzU3IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjE2NSIgc3Ryb2tlLXdpZHRoPSIxLjAzIi8+PGxpbmUgeDE9IjEwIiB5MT0iNTIzIiB4Mj0iMTkiIHkyPSIxODY5IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjE1MCIgc3Ryb2tlLXdpZHRoPSIwLjUzIi8+PGxpbmUgeDE9IjQzNCIgeTE9IjI4NSIgeDI9IjQyMSIgeTI9IjE0NDIiIHN0cm9rZT0icmdiKDYwLDQ4LDM0KSIgc3Ryb2tlLW9wYWNpdHk9IjAuMTU3IiBzdHJva2Utd2lkdGg9IjAuNzAiLz48bGluZSB4MT0iMTExIiB5MT0iMTgwIiB4Mj0iOTUiIHkyPSIxNTk0IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA4NiIgc3Ryb2tlLXdpZHRoPSIwLjkxIi8+PGxpbmUgeDE9IjE0MCIgeTE9IjE2NCIgeDI9IjE1OCIgeTI9Ijc1OCIgc3Ryb2tlPSJyZ2IoNjAsNDgsMzQpIiBzdHJva2Utb3BhY2l0eT0iMC4xMDUiIHN0cm9rZS13aWR0aD0iMS4wNCIvPjxsaW5lIHgxPSIxOTIiIHkxPSIzNjMiIHgyPSIyMDciIHkyPSIxNDg5IiBzdHJva2U9InJnYig2MCw0OCwzNCkiIHN0cm9rZS1vcGFjaXR5PSIwLjA3NSIgc3Ryb2tlLXdpZHRoPSIxLjAzIi8+PC9zdmc+"/><g opacity="0.85"><image x="0" y="0" width="1001" height="1620" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAxIiBoZWlnaHQ9IjE2MjAiIHZpZXdCb3g9IjAgMCAxMDAxIDE2MjAiPgo8ZGVmcz48ZmlsdGVyIGlkPSJnIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj4KPGZlVHVyYnVsZW5jZSB0eXBlPSJmcmFjdGFsTm9pc2UiIGJhc2VGcmVxdWVuY3k9IjAuOSIgbnVtT2N0YXZlcz0iMiIgc2VlZD0iMTkiIHN0aXRjaFRpbGVzPSJzdGl0Y2giIHJlc3VsdD0ibiIvPgo8ZmVDb2xvck1hdHJpeCBpbj0ibiIgdHlwZT0ibWF0cml4IiB2YWx1ZXM9IjAgMCAwIDAgMC4xNDkgMCAwIDAgMCAwLjEyNSAwIDAgMCAwIDAuMTAyIDAgMCAwIDAuNTIgMCIvPgo8L2ZpbHRlcj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMDEiIGhlaWdodD0iMTYyMCIgZmlsdGVyPSJ1cmwoI2cpIi8+PC9zdmc+"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
BIN
fable/Proof1/proof1_letter_8.5x11.png
Normal file
|
After Width: | Height: | Size: 9.4 MiB |
81
fable/Proof1/simulator.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<title>Proof1 · lit chamber — backlit parallax</title>
|
||||
<style>
|
||||
:root{--gap:6;--bright:1;}
|
||||
html,body{margin:0;height:100%;background:#0a0a0b;overflow:hidden;
|
||||
font:12px/1.5 ui-monospace,monospace;color:#9aa;}
|
||||
#wrap{position:fixed;inset:0;display:grid;place-items:center;}
|
||||
.scene{perspective:2200px;perspective-origin:50% 45%;}
|
||||
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
|
||||
width:500.5px;height:810px;}
|
||||
.panel{position:absolute;inset:-14%;transform:translateZ(-120px);
|
||||
background:radial-gradient(120% 90% at 50% 42%,#fff6ec, #ffe7c8 42%, #f0c48c 78%, #caa06a 100%);
|
||||
filter:blur(2px);}
|
||||
.plane{position:absolute;inset:0;width:100%;height:100%;background-size:cover;
|
||||
background-repeat:no-repeat;will-change:transform,filter;}
|
||||
#hud{position:fixed;left:14px;top:12px;z-index:9;background:#141416cc;padding:12px 14px;
|
||||
border-left:3px solid #d9b48a;max-width:300px;}
|
||||
#hud h1{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:#d9b48a;font-weight:400;margin:0 0 8px;}
|
||||
#hud label{display:block;margin:7px 0 2px;color:#aab;}
|
||||
#hud input[type=range]{width:100%;}
|
||||
#legend{position:fixed;right:14px;top:12px;z-index:9;background:#141416cc;padding:10px 12px;color:#889;}
|
||||
#legend b{color:#d9b48a;font-weight:400;}
|
||||
.pill{display:inline-block;margin:2px 4px 2px 0;padding:1px 6px;border:1px solid #333;border-radius:9px;cursor:pointer;color:#aab;}
|
||||
.pill.off{opacity:.35;text-decoration:line-through;}
|
||||
</style></head><body>
|
||||
<div id="hud">
|
||||
<h1>Proof1 · lit chamber</h1>
|
||||
<div>move mouse = parallax · drag sliders</div>
|
||||
<label>air gap ×<span id="gv"></span></label>
|
||||
<input id="gap" type="range" min="1" max="18" step="0.5" value="6">
|
||||
<label>backlight<span id="bv"></span></label>
|
||||
<input id="bright" type="range" min="0.3" max="1.8" step="0.05" value="1">
|
||||
<div id="pills"></div>
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
<div id="wrap"><div class="scene"><div class="stack" id="stack">
|
||||
<div class="panel" id="panel"></div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const PLANES=[{"id":"sun","file":"plane-1-sun.svg","z":0,"blur":1.4,"dim":0.95,"white":"none","glow":1},{"id":"sea-far","file":"plane-2-sea-far.svg","z":9,"blur":0.9,"dim":1,"white":"none","glow":0.7},{"id":"sea-mid","file":"plane-3-sea-mid.svg","z":15,"blur":0.5,"dim":1,"white":"trace","glow":0.7},{"id":"sea-near","file":"plane-4-sea-near.svg","z":18,"blur":0.2,"dim":1,"white":"trace","glow":0.7},{"id":"event-halo","file":"plane-5-event-halo.svg","z":27,"blur":2.4,"dim":0.85,"white":"none","glow":0.9},{"id":"event-core","file":"plane-6-event-core.svg","z":30,"blur":0,"dim":1,"white":"under","glow":0.5},{"id":"front-glass","file":"plane-7-front-glass.svg","z":30,"blur":0,"dim":1,"white":"none","glow":0,"blend":"multiply"}];
|
||||
const stack=document.getElementById('stack');
|
||||
const legend=document.getElementById('legend');
|
||||
const pills=document.getElementById('pills');
|
||||
const maxZ=Math.max(...PLANES.map(p=>p.z));
|
||||
const els={};
|
||||
legend.innerHTML='<b>planes</b> (front → back)<br>'+[...PLANES].reverse().map(p=>p.id+' · '+p.z+'mm').join('<br>');
|
||||
PLANES.forEach(p=>{
|
||||
const d=document.createElement('div');
|
||||
d.className='plane'; d.style.backgroundImage='url('+p.file+')';
|
||||
d.dataset.id=p.id; stack.appendChild(d); els[p.id]=d;
|
||||
const pill=document.createElement('span'); pill.className='pill'; pill.textContent=p.id;
|
||||
pill.onclick=()=>{d.dataset.off=d.dataset.off?'':'1'; pill.classList.toggle('off'); layout();};
|
||||
pills.appendChild(pill);
|
||||
});
|
||||
let gap=6, bright=1, mx=0, my=0;
|
||||
function layout(){
|
||||
for(const p of PLANES){
|
||||
const e=els[p.id];
|
||||
const z=(p.z-maxZ)*gap; // front plane at 0, others recede
|
||||
const blur=(p.blur||0)*(gap/6);
|
||||
const off=e.dataset.off?' opacity:0;':'';
|
||||
e.style.transform='translateZ('+z+'px)';
|
||||
e.style.filter='blur('+blur.toFixed(2)+'px)';
|
||||
e.style.opacity=e.dataset.off?0:(p.dim??1);
|
||||
e.style.mixBlendMode=p.blend||'normal';
|
||||
}
|
||||
document.getElementById('panel').style.filter='blur(2px) brightness('+bright+')';
|
||||
}
|
||||
function parallax(){
|
||||
stack.style.transform='rotateX('+(my*7)+'deg) rotateY('+(mx*9)+'deg)';
|
||||
}
|
||||
addEventListener('mousemove',e=>{
|
||||
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; parallax();
|
||||
});
|
||||
const gapI=document.getElementById('gap'), brI=document.getElementById('bright');
|
||||
gapI.oninput=()=>{gap=+gapI.value; document.getElementById('gv').textContent=' '+gap; layout();};
|
||||
brI.oninput=()=>{bright=+brI.value; document.getElementById('bv').textContent=' '+bright.toFixed(2); layout();};
|
||||
document.getElementById('gv').textContent=' '+gap;
|
||||
document.getElementById('bv').textContent=' '+bright.toFixed(2);
|
||||
layout(); parallax();
|
||||
</script></body></html>
|
||||
29
fable/REFINED_Backlit/01_sun-above.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
29
fable/REFINED_Backlit/02_sun-touching.svg
Normal file
|
After Width: | Height: | Size: 6.7 MiB |
29
fable/REFINED_Backlit/03_sun-half.svg
Normal file
|
After Width: | Height: | Size: 6.9 MiB |
29
fable/REFINED_Backlit/04_sun-drowned.svg
Normal file
|
After Width: | Height: | Size: 7.0 MiB |
29
fable/REFINED_Backlit/05_sun-gone.svg
Normal file
|
After Width: | Height: | Size: 7.2 MiB |
13
fable/REFINED_Backlit/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit · one sun setting</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}
|
||||
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:22px}
|
||||
.notes{color:#999;background:#161210;padding:12px 16px;border-left:3px solid #d9b48a;margin:14px 0;max-width:960px}</style></head><body>
|
||||
<h1>One sun setting — plate Nº 217, five exposures</h1>
|
||||
<div class="notes">The trigger event is identical in every frame — a single millisecond, embalmed — while everything around it ages: the sun walks its setting diagonal and sinks, the chamber keeps running (history accumulates), the grain grows as the light dies, and the water remembers: the wave rows slowly bend around the submerged evidence. The sea never reacts in the instant; influence arrives only through time. The hand tracks the descent — ghost-rings where the sun was, altitude readings, and when the sun is gone, a guess.</div>
|
||||
<div class="row"><figure><img src="01_sun-above.svg"><figcaption>sun above · 19:04</figcaption></figure><figure><img src="02_sun-touching.svg"><figcaption>sun touching · 19:26</figcaption></figure><figure><img src="03_sun-half.svg"><figcaption>sun half · 19:47</figcaption></figure><figure><img src="04_sun-drowned.svg"><figcaption>sun drowned · 20:08</figcaption></figure><figure><img src="05_sun-gone.svg"><figcaption>sun gone · 20:31</figcaption></figure></div>
|
||||
<div class="grid"><figure><img src="01_sun-above.svg"><figcaption>sun above · 19:04</figcaption></figure><figure><img src="02_sun-touching.svg"><figcaption>sun touching · 19:26</figcaption></figure><figure><img src="03_sun-half.svg"><figcaption>sun half · 19:47</figcaption></figure><figure><img src="04_sun-drowned.svg"><figcaption>sun drowned · 20:08</figcaption></figure><figure><img src="05_sun-gone.svg"><figcaption>sun gone · 20:31</figcaption></figure></div>
|
||||
</body></html>
|
||||
28
fable/REFINED_Backlit2/00_reference.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
28
fable/REFINED_Backlit2/01_traces-bold.svg
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
28
fable/REFINED_Backlit2/02_traces-fine.svg
Normal file
|
After Width: | Height: | Size: 6.8 MiB |
28
fable/REFINED_Backlit2/03_sea-broad.svg
Normal file
|
After Width: | Height: | Size: 5.8 MiB |
28
fable/REFINED_Backlit2/04_sea-fine.svg
Normal file
|
After Width: | Height: | Size: 7.7 MiB |
28
fable/REFINED_Backlit2/05_sea-deep.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
28
fable/REFINED_Backlit2/06_sea-etched.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
28
fable/REFINED_Backlit2/07_giant-evidence.svg
Normal file
|
After Width: | Height: | Size: 6.6 MiB |
28
fable/REFINED_Backlit2/08_woodcut.svg
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
9
fable/REFINED_Backlit2/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit2 · sun-above scale studies</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>Sun-above — scale studies (colours & composition held)</h1>
|
||||
<div class="row"><figure><img src="00_reference.svg"><figcaption>reference — baseline (traces 1.0 · rows 46 · overlap 1.9 · line ×1)</figcaption></figure><figure><img src="01_traces-bold.svg"><figcaption>traces-bold — evidence enlarged (bubble size 1.6, density 0.9)</figcaption></figure><figure><img src="02_traces-fine.svg"><figcaption>traces-fine — evidence miniaturised (size 0.62, density 1.3)</figcaption></figure><figure><img src="03_sea-broad.svg"><figcaption>sea-broad — few broad swells (rows 26)</figcaption></figure><figure><img src="04_sea-fine.svg"><figcaption>sea-fine — fine ripple (rows 78, line ×0.75)</figcaption></figure><figure><img src="05_sea-deep.svg"><figcaption>sea-deep — deep undulation (overlap 3.1)</figcaption></figure><figure><img src="06_sea-etched.svg"><figcaption>sea-etched — heavy wave line (line ×2.3)</figcaption></figure><figure><img src="07_giant-evidence.svg"><figcaption>giant-evidence — large ink over fine calm water (size 1.7 · rows 78 · line ×0.7 · overlap 1.6)</figcaption></figure><figure><img src="08_woodcut.svg"><figcaption>woodcut — small ink in a heavy carved sea (size 0.7 · rows 24 · line ×2.4 · overlap 2.6)</figcaption></figure></div>
|
||||
</body></html>
|
||||
24
fable/REFINED_Backlit3/00_bold-base.svg
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
24
fable/REFINED_Backlit3/01_broad-swell.svg
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
24
fable/REFINED_Backlit3/02_fine-ripple.svg
Normal file
|
After Width: | Height: | Size: 7.2 MiB |
24
fable/REFINED_Backlit3/03_deep-heave.svg
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
24
fable/REFINED_Backlit3/04_etched-sea.svg
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
24
fable/REFINED_Backlit3/05_glassy-calm.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
24
fable/REFINED_Backlit3/06_turbulent.svg
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
24
fable/REFINED_Backlit3/07_woodcut.svg
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
24
fable/REFINED_Backlit3/08_giant-ink-calm.svg
Normal file
|
After Width: | Height: | Size: 7.0 MiB |
24
fable/REFINED_Backlit3/09_max-bold.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
9
fable/REFINED_Backlit3/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit3 · bold large-trace studies</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>Sun-above — bold, large traces on top · waves varied · sun faded back</h1>
|
||||
<div class="row"><figure><img src="00_bold-base.svg"><figcaption>bold-base — large ink · sea baseline (rows 40 · overlap 2.2 · line ×1.35)</figcaption></figure><figure><img src="01_broad-swell.svg"><figcaption>broad-swell — few broad swells (rows 24 · overlap 2.6)</figcaption></figure><figure><img src="02_fine-ripple.svg"><figcaption>fine-ripple — dense fine ripple (rows 82 · line ×0.9)</figcaption></figure><figure><img src="03_deep-heave.svg"><figcaption>deep-heave — deep undulation (overlap 3.4)</figcaption></figure><figure><img src="04_etched-sea.svg"><figcaption>etched-sea — heavy carved wave line (line ×2.6)</figcaption></figure><figure><img src="05_glassy-calm.svg"><figcaption>glassy-calm — shallow glassy water (overlap 1.3 · rows 52)</figcaption></figure><figure><img src="06_turbulent.svg"><figcaption>turbulent — turbulent, blip-rich sea (chaos 1.0 · blips 2.2 · overlap 2.8)</figcaption></figure><figure><img src="07_woodcut.svg"><figcaption>woodcut — heavy carved swell (rows 26 · line ×2.8 · overlap 3.0)</figcaption></figure><figure><img src="08_giant-ink-calm.svg"><figcaption>giant-ink-calm — giant evidence over quiet fine water (size 1.9 · rows 78 · line ×0.8 · overlap 1.5)</figcaption></figure><figure><img src="09_max-bold.svg"><figcaption>max-bold — everything loud (size 1.8 · rows 32 · line ×2.4 · overlap 3.0 · chaos 0.95)</figcaption></figure></div>
|
||||
</body></html>
|
||||
24
fable/REFINED_Backlit4/00_golden.svg
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
24
fable/REFINED_Backlit4/01_golden.svg
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
24
fable/REFINED_Backlit4/02_golden.svg
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
24
fable/REFINED_Backlit4/03_golden-mirror.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit4/04_golden-mirror.svg
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
24
fable/REFINED_Backlit4/05_golden-mirror.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
9
fable/REFINED_Backlit4/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit4 · golden</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — golden (1001×1620, φ)</h1>
|
||||
<div class="row"><figure><img src="00_golden.svg"><figcaption>00_golden</figcaption></figure><figure><img src="01_golden.svg"><figcaption>01_golden</figcaption></figure><figure><img src="02_golden.svg"><figcaption>02_golden</figcaption></figure><figure><img src="03_golden-mirror.svg"><figcaption>03_golden-mirror</figcaption></figure><figure><img src="04_golden-mirror.svg"><figcaption>04_golden-mirror</figcaption></figure><figure><img src="05_golden-mirror.svg"><figcaption>05_golden-mirror</figcaption></figure></div>
|
||||
</body></html>
|
||||
24
fable/REFINED_Backlit5/00_golden.svg
Normal file
|
After Width: | Height: | Size: 5.9 MiB |
24
fable/REFINED_Backlit5/01_golden.svg
Normal file
|
After Width: | Height: | Size: 5.3 MiB |
24
fable/REFINED_Backlit5/02_golden.svg
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
24
fable/REFINED_Backlit5/03_golden-mirror.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit5/04_golden-mirror.svg
Normal file
|
After Width: | Height: | Size: 5.6 MiB |
24
fable/REFINED_Backlit5/05_golden-mirror.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
9
fable/REFINED_Backlit5/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit5 · golden · delicate bubbles</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — golden (1001×1620, φ)</h1>
|
||||
<div class="row"><figure><img src="00_golden.svg"><figcaption>00_golden</figcaption></figure><figure><img src="01_golden.svg"><figcaption>01_golden</figcaption></figure><figure><img src="02_golden.svg"><figcaption>02_golden</figcaption></figure><figure><img src="03_golden-mirror.svg"><figcaption>03_golden-mirror</figcaption></figure><figure><img src="04_golden-mirror.svg"><figcaption>04_golden-mirror</figcaption></figure><figure><img src="05_golden-mirror.svg"><figcaption>05_golden-mirror</figcaption></figure></div>
|
||||
</body></html>
|
||||
9
fable/REFINED_Backlit6/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit6 · bubble-opacity sweep</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — bubble-opacity sweep 20→80% (1001×1620, φ)</h1>
|
||||
<div class="row"><figure><img src="op-20.svg"><figcaption>op-20</figcaption></figure><figure><img src="op-32.svg"><figcaption>op-32</figcaption></figure><figure><img src="op-44.svg"><figcaption>op-44</figcaption></figure><figure><img src="op-56.svg"><figcaption>op-56</figcaption></figure><figure><img src="op-68.svg"><figcaption>op-68</figcaption></figure><figure><img src="op-80.svg"><figcaption>op-80</figcaption></figure></div>
|
||||
</body></html>
|
||||
24
fable/REFINED_Backlit6/op-20.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit6/op-32.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit6/op-44.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit6/op-56.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit6/op-68.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
24
fable/REFINED_Backlit6/op-80.svg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
9
fable/REFINED_Backlit7/index.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit6 · bubble-opacity sweep</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — merge · uniform-lace target sweep (1854×3000, φ)</h1>
|
||||
<div class="row"><figure><img src="merge-35.svg"><figcaption>merge-35</figcaption></figure><figure><img src="merge-45.svg"><figcaption>merge-45</figcaption></figure><figure><img src="merge-55.svg"><figcaption>merge-55</figcaption></figure><figure><img src="merge-65.svg"><figcaption>merge-65</figcaption></figure><figure><img src="merge-75.svg"><figcaption>merge-75</figcaption></figure><figure><img src="merge-85.svg"><figcaption>merge-85</figcaption></figure></div>
|
||||
</body></html>
|
||||
24
fable/REFINED_Backlit7/merge-35.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
24
fable/REFINED_Backlit7/merge-45.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
24
fable/REFINED_Backlit7/merge-55.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
24
fable/REFINED_Backlit7/merge-65.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
24
fable/REFINED_Backlit7/merge-75.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
24
fable/REFINED_Backlit7/merge-85.svg
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
67
src/compose/waterline.js
Normal file
@@ -0,0 +1,67 @@
|
||||
/* ============================================================
|
||||
waterline.js — the world is sovereign, and it doesn't notice.
|
||||
Splits track polylines at a horizon line yH (scene coords,
|
||||
[-1,1]) into ABOVE and BELOW sets so a compositor can treat
|
||||
the submerged evidence differently: the sea never reacts to
|
||||
the event; everything that crosses the surface is altered BY
|
||||
the water instead —
|
||||
· a refraction KINK at the crossing (horizontal offset
|
||||
driven by the entry slope),
|
||||
· a slight vertical COMPRESSION of the submerged world
|
||||
toward the surface (optical squash),
|
||||
· (tint / blur / dimming are the compositor's job).
|
||||
β, weight, kind, z, age are preserved across the split so
|
||||
physics-law palettes keep their colour continuity.
|
||||
============================================================ */
|
||||
|
||||
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
|
||||
/* one track → runs of consecutive points on one side, with exact
|
||||
crossing points interpolated onto BOTH adjacent runs */
|
||||
function runsOf(track, yH) {
|
||||
const runs = [];
|
||||
let cur = null;
|
||||
const pts = track.pts;
|
||||
const side = (p) => (p.y < yH ? 'above' : 'below');
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
const p = pts[i];
|
||||
if (!cur) { cur = { side: side(p), pts: [p], entrySlope: 0 }; continue; }
|
||||
if (side(p) === cur.side) { cur.pts.push(p); continue; }
|
||||
// crossing: interpolate the surface point
|
||||
const q = pts[i - 1];
|
||||
const f = (yH - q.y) / (p.y - q.y);
|
||||
const cross = {
|
||||
x: q.x + (p.x - q.x) * f, y: yH,
|
||||
beta: (q.beta ?? 0.5) + ((p.beta ?? 0.5) - (q.beta ?? 0.5)) * f,
|
||||
theta: q.theta ?? 0,
|
||||
};
|
||||
cur.pts.push(cross);
|
||||
runs.push(cur);
|
||||
const slope = (p.x - q.x) / Math.max(1e-6, Math.abs(p.y - q.y));
|
||||
cur = { side: side(p), pts: [{ ...cross }], entrySlope: clamp(slope, -1.4, 1.4) };
|
||||
}
|
||||
if (cur) runs.push(cur);
|
||||
return runs;
|
||||
}
|
||||
|
||||
/* split a track list at the waterline.
|
||||
opts: kink (horizontal refraction offset per unit entry slope),
|
||||
compress (vertical squash of the submerged world, 1 = none) */
|
||||
export function splitTracksAtWaterline(tracks, yH, opts = {}) {
|
||||
const kink = opts.kink ?? 0.016;
|
||||
const comp = opts.compress ?? 0.95;
|
||||
const above = [], below = [];
|
||||
for (const t of tracks) {
|
||||
if (!t.pts || t.pts.length < 2) continue;
|
||||
for (const run of runsOf(t, yH)) {
|
||||
if (run.pts.length < 2) continue;
|
||||
if (run.side === 'above') { above.push({ ...t, pts: run.pts }); continue; }
|
||||
const dx = run.entrySlope * kink;
|
||||
below.push({
|
||||
...t,
|
||||
pts: run.pts.map(p => ({ ...p, x: p.x + dx, y: yH + (p.y - yH) * comp })),
|
||||
});
|
||||
}
|
||||
}
|
||||
return { above, below };
|
||||
}
|
||||
@@ -43,20 +43,36 @@ export function carpetSVG(size, opts = {}) {
|
||||
blips: 1.0, // density of the spiralling wave-packet excitations
|
||||
hue: 0.52, hue2: 0.55, sat: 0.55, lightNear: 0.34, lightFar: 0.62,
|
||||
strokeNear: 1.7, strokeFar: 0.5,
|
||||
// optional field injections (all default OFF → output unchanged):
|
||||
// warpFn(t, d) → extra normalized vertical displacement (×amp) — an
|
||||
// external field (e.g. wind) shaping the rows
|
||||
// phaseFn(t, r) → phase shift added to every base mode (shimmer)
|
||||
// modesFn(rng, o) → replace the base mode set entirely (e.g. a CMB
|
||||
// power-spectrum-shaped spectrum)
|
||||
warpFn: null, phaseFn: null, modesFn: null,
|
||||
}, opts);
|
||||
const W = size, H = size, u = size / 1000;
|
||||
const W = o.width ?? size, H = o.height ?? size, u = size / 1000; // width/height default to a square of `size`
|
||||
const paper = resolveSubstrate(o.substrate).paper.flat;
|
||||
const rng = makeRng(o.seed, o.salt);
|
||||
|
||||
// ---- base sea: low-frequency field modes (phase drifts slowly per row) ----
|
||||
const M = Math.round(4 + o.chaos * 8);
|
||||
const modes = [];
|
||||
for (let m = 0; m < M; m++) {
|
||||
let modes = [];
|
||||
if (o.modesFn) {
|
||||
modes = o.modesFn(rng, o);
|
||||
} else for (let m = 0; m < M; m++) {
|
||||
const f = range(rng, 0.4, 3.0 + o.chaos * 5); // low q: long swells dominate
|
||||
modes.push({ f, a: 1 / (1 + f * 1.0), phi: range(rng, 0, Math.PI * 2), drift: range(rng, -1, 1) * (0.06 + f * 0.02) });
|
||||
}
|
||||
const norm = modes.reduce((s, m) => s + m.a, 0) || 1;
|
||||
|
||||
// coherentRows: express the standing-wave field in a SHARED row coordinate
|
||||
// (this reference count) instead of the layer's own `rows`. Two carpets with
|
||||
// the same seed/salt/chaos/blips but DIFFERENT rows then phase-lock — the
|
||||
// crests and spiralling packets land at the same depth, so a stack of sheets
|
||||
// with different line counts reads as one sea sampled at several resolutions.
|
||||
const REF = o.coherentRows ?? o.rows;
|
||||
|
||||
// ---- coherent excitations: soft wave-packets that drift + rotate phase
|
||||
// across rows, so they SPIRAL through the depth of the stack ----
|
||||
const nExc = Math.round((2 + o.chaos * 5) * o.blips);
|
||||
@@ -64,25 +80,30 @@ export function carpetSVG(size, opts = {}) {
|
||||
for (let e = 0; e < nExc; e++) {
|
||||
exc.push({
|
||||
x0: range(rng, 0.12, 0.88),
|
||||
row0: range(rng, 0, o.rows - 1),
|
||||
span: range(rng, 0.12, 0.3) * o.rows, // rows over which it lives
|
||||
row0: range(rng, 0, REF - 1),
|
||||
span: range(rng, 0.12, 0.3) * REF, // reference rows over which it lives
|
||||
w: range(rng, 0.05, 0.11), // packet width (soft edge)
|
||||
k: range(rng, 3.5, 7), // oscillations within the packet
|
||||
amp: range(rng, 0.30, 0.7),
|
||||
drift: range(rng, -0.018, 0.018), // lateral drift per row
|
||||
drift: range(rng, -0.018, 0.018), // lateral drift per ref-row
|
||||
phase: range(rng, 0, Math.PI * 2),
|
||||
phaseAdv: range(rng, -0.45, 0.45), // phase rotation per row → spiral
|
||||
phaseAdv: range(rng, -0.45, 0.45), // phase rotation per ref-row → spiral
|
||||
sign: rng() < 0.5 ? -1 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
// r is this layer's integer row; rr is the shared reference-row coordinate at
|
||||
// the same depth, so the field is a function of depth, not of line count.
|
||||
const value = (t, r) => {
|
||||
const d = o.rows > 1 ? r / (o.rows - 1) : 0;
|
||||
const rr = d * (REF - 1);
|
||||
let s = 0;
|
||||
for (const m of modes) s += m.a * Math.sin(2 * Math.PI * m.f * t + m.phi + r * m.drift);
|
||||
const dphi = o.phaseFn ? o.phaseFn(t, r) : 0;
|
||||
for (const m of modes) s += m.a * Math.sin(2 * Math.PI * m.f * t + m.phi + rr * m.drift + dphi);
|
||||
s /= norm;
|
||||
let blip = 0;
|
||||
for (const e of exc) {
|
||||
const dr = r - e.row0;
|
||||
const dr = rr - e.row0;
|
||||
const env = Math.exp(-Math.pow(dr / (e.span * 0.5), 2));
|
||||
if (env < 0.02) continue;
|
||||
const cx = e.x0 + e.drift * dr;
|
||||
@@ -112,7 +133,8 @@ export function carpetSVG(size, opts = {}) {
|
||||
for (let i = 0; i <= npts; i++) {
|
||||
const t = i / npts;
|
||||
const e = env(t);
|
||||
pts.push({ x: cx - half + t * 2 * half, y: baseY[r] - amp * e * value(t, r) - amp * 0.55 * (e - eMin) * d });
|
||||
const warp = o.warpFn ? o.warpFn(t, d) * amp : 0;
|
||||
pts.push({ x: cx - half + t * 2 * half, y: baseY[r] - amp * e * value(t, r) - amp * 0.55 * (e - eMin) * d + warp });
|
||||
}
|
||||
const path = smoothPath(pts);
|
||||
const hue = o.hue + (o.hue2 - o.hue) * (1 - d);
|
||||
|
||||
@@ -112,6 +112,25 @@ export function bubbleStops(soft) {
|
||||
const midA = Math.max(0.15, 0.9 - 0.32 * s);
|
||||
return [[0, coreA], [midOff, midA], [1, 0]];
|
||||
}
|
||||
|
||||
// Hollow "hybrid" bubble: a faint paper-tinted veil in the core + a brighter,
|
||||
// SATURATED rim that softens outward — a refracting bubble rather than a filled
|
||||
// disc. Each stop carries [offset, alpha, paperMix]: paperMix 1 = fully toward
|
||||
// paper (pale core), 0 = full family colour (jewel rim). `weight` (0..1) scales
|
||||
// the rim's peak alpha for delicacy; `soft` widens the rim inward.
|
||||
export function bubbleHollowStops(weight = 0.7, soft = 0.3, peakOverride = null) {
|
||||
const s = clamp(soft, 0, 1.6);
|
||||
const peak = peakOverride ?? (0.35 + 0.45 * clamp(weight, 0, 1)); // ~0.7 at weight 0.78
|
||||
const rimOff = clamp(0.80 - 0.14 * s, 0.6, 0.86); // rim sits ~0.7–0.8 of radius
|
||||
return [
|
||||
[0.00, peak * 0.36, 0.74], // pale veil at the very centre
|
||||
[Math.max(0.22, rimOff - 0.5), peak * 0.26, 0.5], // dip — the transparent belly
|
||||
[rimOff - 0.16, peak * 0.72, 0.12], // rim rising, colour saturating
|
||||
[rimOff, peak, 0.0], // bright saturated rim (peak)
|
||||
[Math.min(0.94, rimOff + 0.12), peak * 0.55, 0.04],
|
||||
[1.00, 0.0, 0.0], // feather to nothing
|
||||
];
|
||||
}
|
||||
export const bubbleFoot = (soft) => 2.4 + clamp(soft, 0, 1.6) * 1.5; // sprite footprint grows with softness
|
||||
|
||||
/* ---------- the colour mappings ---------- */
|
||||
@@ -246,6 +265,52 @@ export const PALETTES = {
|
||||
feature: (e) => featureHue(e.inv, e.diskHue ?? 0.06, e.diskSat ?? 0.82), // disk + furniture
|
||||
},
|
||||
|
||||
// magentarise's type-family with the β law living INSIDE each member: kind
|
||||
// chooses the hue (hot magenta primaries, pink δ-rays, cooler purples for
|
||||
// cosmics and V's — the family structure that made seethe-bold's chord), and
|
||||
// within each track β drives transparent birth → the family hue at full
|
||||
// strength → a dive to dark violet only at the very end (dying is slowing).
|
||||
magbeta: {
|
||||
id: 'magbeta', label: 'Magenta family · β within',
|
||||
bubbleInk: (b, e) => {
|
||||
const bb = Math.round(clamp(b.beta ?? 0.5, 0, 1) * 20) / 20;
|
||||
const slow = 1 - bb;
|
||||
const kindH = (((MAG_HUE[b.track.kind] ?? 0.9) + (e.traceHue ?? 0)) % 1 + 1) % 1;
|
||||
const dive = Math.pow(slow, 3.2); // hold the family hue; violet is the last word
|
||||
const h = kindH - (kindH - 0.755) * dive;
|
||||
const s = 0.62 + 0.28 * slow;
|
||||
const l = e.inv ? 0.56 - 0.22 * slow : 0.32 + 0.30 * slow;
|
||||
const c = hslToRgb(h, s, l);
|
||||
const toPaper = Math.pow(bb, 1.4) * 0.7; // fast → dissolves into the substrate
|
||||
return mix(c, e.basePaper.flat, toPaper);
|
||||
},
|
||||
feature: (e) => featureHue(e.inv, e.diskHue ?? 0.06, e.diskSat ?? 0.82),
|
||||
},
|
||||
|
||||
// β-law: the strict physics legend. Opacity is already physics (bubble density
|
||||
// ∝ 1/β² — fast particles barely nucleate), and ink now follows the same law:
|
||||
// born fast = barely tints the PAPER (mixing toward the substrate, not white —
|
||||
// transparent births), slowing = saturating through the magenta family, at
|
||||
// rest = dense violet. "Death earns the violet" emerges without a narrative
|
||||
// rule, because dying IS slowing. Feature/disk defaults to the ember heart.
|
||||
betalaw: {
|
||||
id: 'betalaw', label: 'β law (fast → faint · rest → violet)',
|
||||
bubbleInk: (b, e) => {
|
||||
const bb = Math.round(clamp(b.beta ?? 0.5, 0, 1) * 20) / 20; // quantise: bounded gradients
|
||||
const slow = 1 - bb;
|
||||
// quadratic hue: the family HOLDS hot magenta through most of a life and
|
||||
// dives to violet only at the very end — the violet is the last word, not
|
||||
// the whole sentence.
|
||||
const h = 0.95 - 0.19 * slow * slow;
|
||||
const s = 0.60 + 0.30 * slow;
|
||||
const l = e.inv ? 0.58 - 0.24 * slow : 0.30 + 0.32 * slow;
|
||||
const c = hslToRgb(h, s, l);
|
||||
const toPaper = Math.pow(bb, 1.3) * 0.72; // fast → dissolves into the substrate
|
||||
return mix(c, e.basePaper.flat, toPaper);
|
||||
},
|
||||
feature: (e) => featureHue(e.inv, e.diskHue ?? 0.02, e.diskSat ?? 0.88),
|
||||
},
|
||||
|
||||
// a complete "chemistry": overrides paper + ink together (deep Prussian-blue
|
||||
// ground, pale lines) — the blueprint/cyanotype look.
|
||||
cyanotype: {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
============================================================ */
|
||||
import { makeRng, cyrb53 } from '../rng.js';
|
||||
import { sampleBubbles, trackInkWeight, depthFactors } from '../scene/bubbles.js';
|
||||
import { resolvePalette, paperTone, rgbHex, rgbKey, bubbleStops, hslToRgb, mix } from './palette.js';
|
||||
import { resolvePalette, paperTone, rgbHex, rgbKey, bubbleStops, bubbleHollowStops, hslToRgb, mix } from './palette.js';
|
||||
|
||||
const MARGIN = 0.02;
|
||||
|
||||
@@ -37,12 +37,18 @@ function rgb2hsl([r, g, b]) {
|
||||
}
|
||||
|
||||
export function renderSVG(scene, params, sizePx = 4800) {
|
||||
const w = sizePx, h = sizePx;
|
||||
const scale = (w / 2) * (1 - MARGIN);
|
||||
const cx = w / 2, cy = h / 2;
|
||||
// Non-square canvas support (defaults preserve exact square behaviour):
|
||||
// canvasW/canvasH → explicit page size · originX/originY → scene origin (px)
|
||||
// scale stays isotropic (min dimension) so the event never distorts; the
|
||||
// scene origin can be placed anywhere on the page (e.g. a golden intersection).
|
||||
const w = params.canvasW ?? sizePx, h = params.canvasH ?? sizePx;
|
||||
// sceneZoom > 1 magnifies the whole event about its origin (bubbles AND spray
|
||||
// extent grow together); content may spill past the page edge. Default 1 = unchanged.
|
||||
const scale = (Math.min(w, h) / 2) * (1 - MARGIN) * (params.sceneZoom ?? 1);
|
||||
const cx = params.originX ?? w / 2, cy = params.originY ?? h / 2;
|
||||
const tx = (x) => (cx + x * scale).toFixed(2);
|
||||
const ty = (y) => (cy + y * scale).toFixed(2);
|
||||
const u = w / 1000;
|
||||
const u = Math.min(w, h) / 1000;
|
||||
|
||||
const inv = params.invert;
|
||||
// base ink (mono), toned paper (independent of ink palette), then resolve feel
|
||||
@@ -60,7 +66,10 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
||||
// so the centre reads as compressed colour rather than a hole.
|
||||
const press = Math.max(0, Math.min(1, params.diskPressure ?? 0));
|
||||
const [fh, fs, fl] = rgb2hsl(pal.feature());
|
||||
const coreInk = rgbHex(hslToRgb(fh, Math.min(1, fs * (1 + 0.25 * press)), fl * (1 - 0.6 * press)));
|
||||
// diskHollow winds the compression back: the core ink relaxes toward the
|
||||
// feature hue as the centre opens (a grey plug would just become a grey ring)
|
||||
const hol0 = Math.max(0, Math.min(1, params.diskHollow ?? 0));
|
||||
const coreInk = rgbHex(hslToRgb(fh, Math.min(1, fs * (1 + 0.25 * press)), fl * (1 - 0.6 * press * (1 - 0.75 * hol0))));
|
||||
const featKey = rgbKey(pal.feature());
|
||||
const colorMap = new Map([[featKey, pal.feature()]]); // distinct bubble colours → gradients
|
||||
|
||||
@@ -155,6 +164,12 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
||||
const buckets = new Map(); // layerId -> Map(colorKey|alpha -> {key, alpha, arr})
|
||||
const ensure = (id) => { if (!under.has(id)) { under.set(id, ''); buckets.set(id, new Map()); } };
|
||||
|
||||
const bop = params.bubbleOpacity ?? 1; // master bubble opacity (a single slider)
|
||||
const hollowOn = (params.bubbleHollow ?? 0) > 0;
|
||||
// bubbleMerge: crowded bubbles fuse into ONE flat union at a uniform density
|
||||
// (group opacity isolates → overlaps don't accumulate). Dense knots become
|
||||
// even lace, not mud — even ink density = even backlit transmission.
|
||||
const merge = !!params.bubbleMerge;
|
||||
const bubbleRng = makeRng(params.seed, 'bubbles'); // scene.tracks order → deterministic
|
||||
for (const track of scene.tracks) {
|
||||
if (track.pts.length < 2) continue;
|
||||
@@ -162,16 +177,18 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
||||
ensure(id);
|
||||
const df = depthFactors(track, params);
|
||||
const repCol = pal.ink(track); // representative ink (under-stroke)
|
||||
// continuity under-stroke
|
||||
// continuity under-stroke. When bubbles are hollow, thin this solid core
|
||||
// hard so the delicate rings carry the trail instead of a filled spine.
|
||||
const iw = trackInkWeight(track);
|
||||
const lw = Math.min(2.6, 0.25 + Math.sqrt(iw) * 0.12) * u * params.size * track.weight;
|
||||
if (lw >= 0.2 * u) {
|
||||
const lw = Math.min(2.6, 0.25 + Math.sqrt(iw) * 0.12) * u * params.size * track.weight * (hollowOn ? 0.6 : 1);
|
||||
if (lw >= 0.2 * u && !merge) { // merge carries continuity via the union itself
|
||||
let d = `M ${tx(track.pts[0].x)} ${ty(track.pts[0].y)}`;
|
||||
for (let i = 1; i < track.pts.length; i++) d += ` L ${tx(track.pts[i].x)} ${ty(track.pts[i].y)}`;
|
||||
under.set(id, under.get(id) + `<path d="${d}" stroke="${rgbHex(repCol)}" stroke-opacity="${(0.14 * df.tone).toFixed(3)}" stroke-width="${lw.toFixed(2)}"/>`);
|
||||
const uop = 0.14 * df.tone * bop * (hollowOn ? 0.4 : 1);
|
||||
under.set(id, under.get(id) + `<path d="${d}" stroke="${rgbHex(repCol)}" stroke-opacity="${uop.toFixed(3)}" stroke-width="${lw.toFixed(2)}"/>`);
|
||||
}
|
||||
// bubbles — opacity from depth/age tone, colour per-bubble from palette
|
||||
const alpha = Math.round(Math.min(1, 0.36 + df.tone * 0.58) * 20) / 20;
|
||||
// bubbles — merge: uniform bucket (density capped by the group). else: opacity from depth/age tone × master.
|
||||
const alpha = merge ? 1 : Math.round(Math.min(1, (0.36 + df.tone * 0.58) * bop) * 100) / 100;
|
||||
const m = buckets.get(id);
|
||||
for (const b of sampleBubbles({ ...track, sizeScale: df.sizeScale }, params, bubbleRng)) {
|
||||
const bcol = pal.bubbleInk(track, b.life, b.beta), bkc = rgbKey(bcol);
|
||||
@@ -187,7 +204,11 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
||||
const us = under.get(L.id);
|
||||
if (us) content += `<g fill="none" stroke-linecap="round" stroke-linejoin="round">${us}</g>\n`;
|
||||
for (const { key, alpha, arr } of buckets.get(L.id).values()) {
|
||||
if (arr.length) content += `<g fill="url(#bub-${key})" fill-opacity="${alpha}">${arr.join('')}</g>\n`;
|
||||
if (!arr.length) continue;
|
||||
// merge: one isolated group at the uniform target → overlaps stay flat (no mud).
|
||||
content += merge
|
||||
? `<g fill="url(#bub-${key})" opacity="${bop.toFixed(2)}" style="isolation:isolate">${arr.join('')}</g>\n`
|
||||
: `<g fill="url(#bub-${key})" fill-opacity="${alpha}">${arr.join('')}</g>\n`;
|
||||
}
|
||||
return layer(L.id, L.label, content);
|
||||
}).join('');
|
||||
@@ -309,7 +330,7 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
||||
let s = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
||||
s += `<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">\n`;
|
||||
s += `<metadata>Bubble Chamber · seed=${params.seed} · hash=${cyrb53(params.seed)} · palette=${params.palette || 'mono'}</metadata>\n`;
|
||||
s += defs({ paperC, ink, coreInk, press, baseVign: pal.vign(), params, u, colorMap });
|
||||
s += defs({ paperC, paperFlat: paperRGB.flat, ink, coreInk, press, baseVign: pal.vign(), params, u, colorMap });
|
||||
// emit: optional array of layer-GROUPS to include, so the plate can be rendered
|
||||
// as decoupled sub-layers — 'background' | 'disk' | 'bubble' | 'fiduciaries'.
|
||||
const emit = params.emit;
|
||||
@@ -327,20 +348,53 @@ export function renderSVG(scene, params, sizePx = 4800) {
|
||||
return s;
|
||||
}
|
||||
|
||||
function defs({ paperC, ink, coreInk, press = 0, baseVign, params, u, colorMap }) {
|
||||
// diskHollow: 0 = pressure plug (dense core), 1 = the centre dissolves to the
|
||||
// paper — density gathers into an ember RING and the heart is left open (for
|
||||
// backlit media the unprinted centre passes lamp light: the sun glows).
|
||||
function shockcoreStops({ coreInk, ink, press, hollow }) {
|
||||
const hol = Math.max(0, Math.min(1, hollow ?? 0));
|
||||
const core = coreInk || ink;
|
||||
const a0 = 0.5 + press * 0.45, a1 = 0.32 + press * 0.32;
|
||||
if (hol <= 0) return `<stop offset="0%" stop-color="${core}" stop-opacity="${a0.toFixed(2)}"/>
|
||||
<stop offset="28%" stop-color="${core}" stop-opacity="${a1.toFixed(2)}"/>
|
||||
<stop offset="62%" stop-color="${ink}" stop-opacity="0.28"/>
|
||||
<stop offset="100%" stop-color="${ink}" stop-opacity="0"/>`;
|
||||
return `<stop offset="0%" stop-color="${core}" stop-opacity="${(a0 * (1 - hol)).toFixed(2)}"/>
|
||||
<stop offset="${Math.round(16 + 8 * hol)}%" stop-color="${core}" stop-opacity="${(a0 * (1 - 0.72 * hol)).toFixed(2)}"/>
|
||||
<stop offset="${Math.round(28 + 12 * hol)}%" stop-color="${core}" stop-opacity="${(a1 * (1 - 0.45 * hol)).toFixed(2)}"/>
|
||||
<stop offset="62%" stop-color="${ink}" stop-opacity="0.28"/>
|
||||
<stop offset="100%" stop-color="${ink}" stop-opacity="0"/>`;
|
||||
}
|
||||
|
||||
function defs({ paperC, paperFlat, ink, coreInk, press = 0, baseVign, params, u, colorMap }) {
|
||||
const soften = params.diskSoften > 0
|
||||
? `<filter id="soften" x="-20%" y="-20%" width="140%" height="140%"><feGaussianBlur stdDeviation="${(params.diskSoften * u).toFixed(2)}"/></filter>`
|
||||
: '';
|
||||
// one soft-bubble gradient per distinct ink colour used (edge profile shared
|
||||
// with the raster sprite via bubbleStops)
|
||||
const stops = bubbleStops(params.bubbleSoft ?? 0.3);
|
||||
// one bubble gradient per distinct ink colour. Default: soft filled disc
|
||||
// (edge profile shared with the raster sprite via bubbleStops). Opt-in
|
||||
// bubbleHollow>0: a hollow refracting bubble — pale core, saturated rim.
|
||||
const hollow = params.bubbleHollow ?? 0;
|
||||
let bubGrads = '';
|
||||
if (hollow > 0) {
|
||||
// merge wants a near-opaque rim so union coverage is flat within the group
|
||||
const hy = bubbleHollowStops(params.bubbleWeight ?? 0.7, params.bubbleSoft ?? 0.3, params.bubbleMerge ? 0.97 : null);
|
||||
for (const [key, col] of colorMap) {
|
||||
let st = '';
|
||||
for (const [off, a, pm] of hy) {
|
||||
const c = rgbHex(mix(col, paperFlat, pm * hollow)); // rim = family colour, core → paper
|
||||
st += `<stop offset="${(off * 100).toFixed(0)}%" stop-color="${c}" stop-opacity="${a.toFixed(3)}"/>`;
|
||||
}
|
||||
bubGrads += `<radialGradient id="bub-${key}" cx="50%" cy="50%" r="50%">${st}</radialGradient>`;
|
||||
}
|
||||
} else {
|
||||
const stops = bubbleStops(params.bubbleSoft ?? 0.3);
|
||||
for (const [key, col] of colorMap) {
|
||||
const c = rgbHex(col);
|
||||
let st = '';
|
||||
for (const [off, a] of stops) st += `<stop offset="${(off * 100).toFixed(0)}%" stop-color="${c}" stop-opacity="${a.toFixed(3)}"/>`;
|
||||
bubGrads += `<radialGradient id="bub-${key}" cx="50%" cy="50%" r="50%">${st}</radialGradient>`;
|
||||
}
|
||||
}
|
||||
const vignHex = rgbHex(baseVign);
|
||||
return `<defs>
|
||||
${soften}
|
||||
@@ -349,10 +403,7 @@ function defs({ paperC, ink, coreInk, press = 0, baseVign, params, u, colorMap }
|
||||
</radialGradient>
|
||||
${bubGrads}
|
||||
<radialGradient id="shockcore" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="${coreInk || ink}" stop-opacity="${(0.5 + press * 0.45).toFixed(2)}"/>
|
||||
<stop offset="28%" stop-color="${coreInk || ink}" stop-opacity="${(0.32 + press * 0.32).toFixed(2)}"/>
|
||||
<stop offset="62%" stop-color="${ink}" stop-opacity="0.28"/>
|
||||
<stop offset="100%" stop-color="${ink}" stop-opacity="0"/>
|
||||
${shockcoreStops({ coreInk, ink, press, hollow: params.diskHollow })}
|
||||
</radialGradient>
|
||||
<radialGradient id="shockstain" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="${ink}" stop-opacity="0.9"/>
|
||||
|
||||
105
src/scene/chinagraph.js
Normal file
@@ -0,0 +1,105 @@
|
||||
/* ============================================================
|
||||
chinagraph.js — the fictional scanner's hand, done right.
|
||||
Letterforms built as STROKES (no fonts anywhere): each glyph
|
||||
is a set of polylines in a unit box, rendered with seeded
|
||||
wobble, per-character baseline drift and rotation, and a
|
||||
waxy double-pass — the soft fat grease pencil of a 1955–85
|
||||
scanning table. Seeded per plate: one plate's hand is
|
||||
consistent with itself, no two plates' hands identical.
|
||||
Charset is the scanner's working set: digits, Nº, θ, °, ·,
|
||||
colon, dash, question — measurement, not prose.
|
||||
============================================================ */
|
||||
import { gauss } from '../rng.js';
|
||||
|
||||
/* glyphs in a unit box, y down; each = array of polylines */
|
||||
const ellipse = (cx, cy, rx, ry, n = 11, a0 = -0.4) => {
|
||||
const pts = [];
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const a = a0 + (i / n) * Math.PI * 2 * 1.03; // slight overshoot — doesn't close cleanly
|
||||
pts.push([cx + Math.cos(a) * rx, cy + Math.sin(a) * ry]);
|
||||
}
|
||||
return pts;
|
||||
};
|
||||
const GLYPHS = {
|
||||
'0': [ellipse(0.48, 0.5, 0.3, 0.44)],
|
||||
'1': [[[0.3, 0.26], [0.55, 0.08], [0.55, 0.95]]],
|
||||
'2': [[[0.16, 0.3], [0.2, 0.12], [0.5, 0.05], [0.76, 0.16], [0.77, 0.36], [0.15, 0.95], [0.85, 0.93]]],
|
||||
'3': [[[0.16, 0.13], [0.6, 0.06], [0.8, 0.25], [0.53, 0.46]], [[0.53, 0.46], [0.84, 0.68], [0.6, 0.93], [0.15, 0.87]]],
|
||||
'4': [[[0.64, 0.08], [0.13, 0.66], [0.87, 0.66]], [[0.64, 0.3], [0.64, 0.95]]],
|
||||
'5': [[[0.8, 0.08], [0.22, 0.09], [0.17, 0.46], [0.55, 0.4], [0.82, 0.6], [0.6, 0.92], [0.17, 0.87]]],
|
||||
'6': [[[0.7, 0.07], [0.32, 0.34], [0.17, 0.64], [0.3, 0.92], [0.64, 0.9], [0.75, 0.66], [0.52, 0.5], [0.22, 0.62]]],
|
||||
'7': [[[0.15, 0.1], [0.85, 0.09], [0.42, 0.95]]],
|
||||
'8': [[[0.5, 0.08], [0.25, 0.2], [0.31, 0.42], [0.5, 0.5], [0.72, 0.63], [0.66, 0.88], [0.4, 0.92], [0.27, 0.79], [0.35, 0.56], [0.5, 0.5], [0.71, 0.36], [0.67, 0.15], [0.5, 0.08]]],
|
||||
'9': [[[0.74, 0.38], [0.5, 0.5], [0.27, 0.44], [0.22, 0.2], [0.46, 0.07], [0.7, 0.13], [0.75, 0.4], [0.6, 0.7], [0.35, 0.94]]],
|
||||
'N': [[[0.1, 0.95], [0.1, 0.08]], [[0.1, 0.1], [0.8, 0.93]], [[0.8, 0.95], [0.8, 0.08]]],
|
||||
'º': [ellipse(0.42, 0.24, 0.17, 0.17, 8), [[0.14, 0.6], [0.72, 0.58]]],
|
||||
'°': [ellipse(0.45, 0.18, 0.14, 0.14, 8)],
|
||||
'θ': [ellipse(0.48, 0.5, 0.28, 0.44), [[0.22, 0.5], [0.75, 0.5]]],
|
||||
':': [[[0.48, 0.3], [0.51, 0.33]], [[0.48, 0.72], [0.51, 0.75]]],
|
||||
'·': [[[0.48, 0.5], [0.52, 0.53]]],
|
||||
'-': [[[0.15, 0.52], [0.8, 0.5]]],
|
||||
'+': [[[0.16, 0.5], [0.8, 0.49]], [[0.48, 0.18], [0.49, 0.82]]],
|
||||
'h': [[[0.16, 0.08], [0.16, 0.95]], [[0.16, 0.5], [0.4, 0.36], [0.66, 0.42], [0.7, 0.95]]],
|
||||
'?': [[[0.2, 0.24], [0.32, 0.08], [0.62, 0.06], [0.78, 0.22], [0.68, 0.44], [0.48, 0.55], [0.48, 0.7]], [[0.47, 0.88], [0.5, 0.91]]],
|
||||
' ': [],
|
||||
};
|
||||
|
||||
/* render a string as chinagraph strokes.
|
||||
o: { x, y px origin (baseline-left)
|
||||
h char height px
|
||||
rng seeded stream (one per plate → consistent hand)
|
||||
ink css colour
|
||||
width base stroke width px
|
||||
slant italic shear (default slight) }
|
||||
Returns an SVG fragment (a <g>). */
|
||||
export function chinagraphText(str, o) {
|
||||
const { x, y, h, rng, ink } = o;
|
||||
const w0 = o.width ?? h * 0.11;
|
||||
const slant = o.slant ?? 0.07;
|
||||
const cw = h * 0.72;
|
||||
let cx = x;
|
||||
let out = '';
|
||||
for (const ch of String(str)) {
|
||||
const glyph = GLYPHS[ch] ?? GLYPHS['·'];
|
||||
const rot = gauss(rng) * 0.035;
|
||||
const dy = gauss(rng) * 0.045 * h;
|
||||
const cos = Math.cos(rot), sin = Math.sin(rot);
|
||||
for (const stroke of glyph) {
|
||||
if (!stroke.length) continue;
|
||||
// two passes: the waxy body + a lighter ghost slightly offset (pencil drag)
|
||||
for (let pass = 0; pass < 2; pass++) {
|
||||
const off = pass * 0.05 * h, op = pass ? 0.3 : 0.82;
|
||||
const wv = w0 * (0.8 + rng() * 0.45);
|
||||
let d = '';
|
||||
stroke.forEach((pt, i) => {
|
||||
let gx = (pt[0] - slant * (1 - pt[1])) * cw * 1.18;
|
||||
let gy = (pt[1] - 0.9) * h;
|
||||
const rx = gx * cos - gy * sin, ry = gx * sin + gy * cos;
|
||||
const jx = gauss(rng) * 0.013 * h, jy = gauss(rng) * 0.013 * h;
|
||||
d += `${i ? 'L' : 'M'} ${(cx + rx + jx + off).toFixed(1)} ${(y + dy + ry + jy + off * 0.4).toFixed(1)} `;
|
||||
});
|
||||
out += `<path d="${d}" fill="none" stroke="${ink}" stroke-opacity="${op}" stroke-width="${wv.toFixed(2)}" stroke-linecap="round" stroke-linejoin="round"/>`;
|
||||
}
|
||||
}
|
||||
cx += cw * (ch === '·' || ch === ':' ? 0.55 : ch === ' ' ? 0.6 : 1);
|
||||
}
|
||||
return `<g>${out}</g>`;
|
||||
}
|
||||
|
||||
/* a wobbly grease arrow (shaft + open head), same hand */
|
||||
export function chinagraphArrow(x1, y1, x2, y2, o) {
|
||||
const { rng, ink } = o;
|
||||
const w = o.width ?? 3;
|
||||
const seg = 8;
|
||||
let d = '';
|
||||
for (let i = 0; i <= seg; i++) {
|
||||
const t = i / seg;
|
||||
const px = x1 + (x2 - x1) * t + gauss(rng) * 2.2;
|
||||
const py = y1 + (y2 - y1) * t + gauss(rng) * 2.2;
|
||||
d += `${i ? 'L' : 'M'} ${px.toFixed(1)} ${py.toFixed(1)} `;
|
||||
}
|
||||
const ang = Math.atan2(y2 - y1, x2 - x1), hl = Math.hypot(x2 - x1, y2 - y1) * 0.14;
|
||||
const head = (da) => `M ${x2.toFixed(1)} ${y2.toFixed(1)} L ${(x2 - Math.cos(ang + da) * hl + gauss(rng) * 1.5).toFixed(1)} ${(y2 - Math.sin(ang + da) * hl + gauss(rng) * 1.5).toFixed(1)} `;
|
||||
return `<g fill="none" stroke="${ink}" stroke-linecap="round" stroke-opacity="0.8" stroke-width="${w}">`
|
||||
+ `<path d="${d}"/><path d="${head(0.42)}${head(-0.42)}"/></g>`;
|
||||
}
|
||||
266
tools/proof1-render.mjs
Normal file
@@ -0,0 +1,266 @@
|
||||
/* ============================================================
|
||||
proof1-render.mjs — PROOF 1: the lit chamber, split into planes.
|
||||
Takes the locked golden merge composition (REFINED_Backlit7,
|
||||
the mirror event, uniform-lace merge) and emits it as SEPARATE
|
||||
transparent planes for a backlit acrylic stack, plus a manifest
|
||||
and a parallax/backlight simulator to art-direct the depth
|
||||
BEFORE committing to print.
|
||||
|
||||
Plane map (deepest → nearest), causal depth cause→world→evidence:
|
||||
1 sun faded disk · pure transmitted colour · zero white
|
||||
2 sea-far sheet[0]
|
||||
3 sea-mid sheet[1]
|
||||
4 sea-near sheet[2]
|
||||
5 event-halo merged bubbles, blurred by the air gap (glow)
|
||||
6 event-core merged bubbles, sharp · white underlay
|
||||
+ front-glass grain + aging (the surface, multiply)
|
||||
|
||||
Golden frame (φ) is preserved and FLOATED in an A3 sheet later;
|
||||
this stage renders the art square only.
|
||||
|
||||
Usage: node tools/proof1-render.mjs [width]
|
||||
→ fable/Proof1/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
|
||||
const PHI = (1 + Math.sqrt(5)) / 2;
|
||||
const IP = 1 / PHI, IP2 = 1 / (PHI * PHI);
|
||||
|
||||
const W = +(process.argv[2] || 1001);
|
||||
const H = Math.round(W * PHI); // golden portrait
|
||||
const OUT = 'fable/Proof1';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = Math.min(W, H) / 1000;
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- golden armature (identical to REFINED_Backlit7) ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const SEED_EVENT = `${SEED}#03`; // the chosen mirror event
|
||||
const BUBOP = +(process.env.BUBOP ?? 0.5); // mid of the 45→55 band
|
||||
const HORIZON_F = IP2;
|
||||
const HY = HORIZON_F * H, SKY_H = HY;
|
||||
const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02);
|
||||
const XL = IP2 * W, XR = IP * W;
|
||||
const SUN = { x: XR, y: IP * SKY_H }; // mirror: sun on the RIGHT line
|
||||
const SUN_R = IP2 * SKY_H;
|
||||
const VERTEX = { x: XL, y: HY }; // mirror: vertex on the LEFT line
|
||||
const TRACE_ZOOM = 2.1;
|
||||
|
||||
/* ---- textures ---- */
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
let lines = '';
|
||||
// deterministic scratch scatter without Math.random (seeded LCG)
|
||||
let s = seed * 2654435761 >>> 0;
|
||||
const rnd = () => ((s = (s * 1664525 + 1013904223) >>> 0) / 4294967296);
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rnd() * W, y0 = rnd() * H * 0.4, len = (0.3 + rnd() * 0.6) * H;
|
||||
const x2 = x + (rnd() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rnd() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rnd() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" filter="url(#fox)"/>
|
||||
<rect width="${W}" height="${H}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- golden sea deck (equal-line, φ waves) ---- */
|
||||
function deck() {
|
||||
const base = {
|
||||
mode: 'plate', width: W, height: H, horizon: HORIZON_F,
|
||||
wFar: 0.5, wNear: 0.5 * PHI, overlap: PHI,
|
||||
rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
|
||||
chaos: 0.82, blips: 1.5,
|
||||
};
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(H, { ...base, salt: 'mfield' + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(0.82 * 0.8, 0.82, t),
|
||||
strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- event params (merge, delicate, golden-placed) ---- */
|
||||
function bcAssembleParams() {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4,
|
||||
shockStriations: 1.0, diskHollow: 1.0,
|
||||
depth: 0.3, aging: 0.28,
|
||||
eventX: 0, eventY: 0, shockX: 0, shockY: 0,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2,
|
||||
shockSize: SUN_R / S_SCALE, shockIntensity: 0.8,
|
||||
size: 1.6,
|
||||
bubbleHollow: 1, bubbleWeight: 0.7, bubbleSoft: 0.7, bubbleMerge: 1,
|
||||
bubbleOpacity: BUBOP,
|
||||
canvasW: W, canvasH: H,
|
||||
seed: SEED_EVENT,
|
||||
});
|
||||
return p;
|
||||
}
|
||||
|
||||
/* ---- render the source images ---- */
|
||||
const p = bcAssembleParams();
|
||||
const scene = generateScene(p);
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
|
||||
const tracksImg = renderSVG({ ...bare, tracks: scene.tracks, shock: null },
|
||||
{ ...p, emit: ['bubble'], originX: VERTEX.x, originY: VERTEX.y, sceneZoom: TRACE_ZOOM }, W);
|
||||
const diskImg = renderSVG({ ...bare, tracks: [], shock: scene.shock },
|
||||
{ ...p, emit: ['disk'], originX: SUN.x, originY: SUN.y }, W);
|
||||
const sheets = deck();
|
||||
const grain = grainSVG({ amount: 0.52 });
|
||||
const aging = agingSVG({ seed: 8 });
|
||||
|
||||
/* ---- write each plane as a transparent SVG (art square only) ---- */
|
||||
const planeSVG = (inner) =>
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">${inner}</svg>`;
|
||||
const IMG = (svg) => `<image x="0" y="0" width="${W}" height="${H}" href="${dataUri(svg)}"/>`;
|
||||
|
||||
// grain+aging combined onto one front-glass texture (multiply handled in sim)
|
||||
const frontGlass = planeSVG(
|
||||
`${IMG(aging)}<g opacity="0.85">${IMG(grain)}</g>`);
|
||||
|
||||
// DOF blur is tuned per MARK TYPE: thin wave-lines dissolve under blur far
|
||||
// faster than dense bubbles, so line-art planes get very little (parallax
|
||||
// offset carries their depth). Bubbles/glow tolerate more.
|
||||
const PLANES = [
|
||||
{ id: 'sun', file: 'plane-1-sun.svg', z: 0, blur: 1.4, dim: 0.95, white: 'none', glow: 1.0, svg: planeSVG(IMG(diskImg)) },
|
||||
{ id: 'sea-far', file: 'plane-2-sea-far.svg', z: 9, blur: 0.9, dim: 1.0, white: 'none', glow: 0.7, svg: planeSVG(IMG(sheets[0])) },
|
||||
{ id: 'sea-mid', file: 'plane-3-sea-mid.svg', z: 15, blur: 0.5, dim: 1.0, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[1])) },
|
||||
{ id: 'sea-near', file: 'plane-4-sea-near.svg', z: 18, blur: 0.2, dim: 1.0, white: 'trace', glow: 0.7, svg: planeSVG(IMG(sheets[2])) },
|
||||
{ id: 'event-halo', file: 'plane-5-event-halo.svg', z: 27, blur: 2.4, dim: 0.85, white: 'none', glow: 0.9, svg: planeSVG(IMG(tracksImg)) },
|
||||
{ id: 'event-core', file: 'plane-6-event-core.svg', z: 30, blur: 0.0, dim: 1.0, white: 'under', glow: 0.5, svg: planeSVG(IMG(tracksImg)) },
|
||||
{ id: 'front-glass',file: 'plane-7-front-glass.svg',z: 30, blur: 0.0, dim: 1.0, white: 'none', glow: 0.0, blend: 'multiply', svg: frontGlass },
|
||||
];
|
||||
|
||||
for (const pl of PLANES) writeFileSync(`${OUT}/${pl.file}`, pl.svg);
|
||||
|
||||
const manifest = {
|
||||
piece: 'Proof1 — the lit chamber',
|
||||
source: 'REFINED_Backlit7 merge (mirror event, uniform-lace)',
|
||||
seed: SEED_EVENT, bubbleOpacity: BUBOP,
|
||||
art: { w: W, h: H, ratio: 'phi (golden portrait)' },
|
||||
sheet: { w_mm: 330, h_mm: 420, note: 'A3 — golden art square floated, clear margins = lit void' },
|
||||
gaps_mm: 'plane z is nominal; two scales: ~3mm within a sheet, ~9mm between',
|
||||
planes: PLANES.map(({ svg, ...rest }) => rest),
|
||||
};
|
||||
writeFileSync(`${OUT}/manifest.json`, JSON.stringify(manifest, null, 2));
|
||||
|
||||
/* ---- the backlit parallax simulator ---- */
|
||||
const planesJSON = JSON.stringify(PLANES.map(({ svg, ...r }) => r));
|
||||
writeFileSync(`${OUT}/simulator.html`, `<!DOCTYPE html><html><head><meta charset="utf-8">
|
||||
<title>Proof1 · lit chamber — backlit parallax</title>
|
||||
<style>
|
||||
:root{--gap:6;--bright:1;}
|
||||
html,body{margin:0;height:100%;background:#0a0a0b;overflow:hidden;
|
||||
font:12px/1.5 ui-monospace,monospace;color:#9aa;}
|
||||
#wrap{position:fixed;inset:0;display:grid;place-items:center;}
|
||||
.scene{perspective:2200px;perspective-origin:50% 45%;}
|
||||
.stack{position:relative;transform-style:preserve-3d;transition:transform .06s linear;
|
||||
width:${W / 2}px;height:${H / 2}px;}
|
||||
.panel{position:absolute;inset:-14%;transform:translateZ(-120px);
|
||||
background:radial-gradient(120% 90% at 50% 42%,#fff6ec, #ffe7c8 42%, #f0c48c 78%, #caa06a 100%);
|
||||
filter:blur(2px);}
|
||||
.plane{position:absolute;inset:0;width:100%;height:100%;background-size:cover;
|
||||
background-repeat:no-repeat;will-change:transform,filter;}
|
||||
#hud{position:fixed;left:14px;top:12px;z-index:9;background:#141416cc;padding:12px 14px;
|
||||
border-left:3px solid #d9b48a;max-width:300px;}
|
||||
#hud h1{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:#d9b48a;font-weight:400;margin:0 0 8px;}
|
||||
#hud label{display:block;margin:7px 0 2px;color:#aab;}
|
||||
#hud input[type=range]{width:100%;}
|
||||
#legend{position:fixed;right:14px;top:12px;z-index:9;background:#141416cc;padding:10px 12px;color:#889;}
|
||||
#legend b{color:#d9b48a;font-weight:400;}
|
||||
.pill{display:inline-block;margin:2px 4px 2px 0;padding:1px 6px;border:1px solid #333;border-radius:9px;cursor:pointer;color:#aab;}
|
||||
.pill.off{opacity:.35;text-decoration:line-through;}
|
||||
</style></head><body>
|
||||
<div id="hud">
|
||||
<h1>Proof1 · lit chamber</h1>
|
||||
<div>move mouse = parallax · drag sliders</div>
|
||||
<label>air gap ×<span id="gv"></span></label>
|
||||
<input id="gap" type="range" min="1" max="18" step="0.5" value="6">
|
||||
<label>backlight<span id="bv"></span></label>
|
||||
<input id="bright" type="range" min="0.3" max="1.8" step="0.05" value="1">
|
||||
<div id="pills"></div>
|
||||
</div>
|
||||
<div id="legend"></div>
|
||||
<div id="wrap"><div class="scene"><div class="stack" id="stack">
|
||||
<div class="panel" id="panel"></div>
|
||||
</div></div></div>
|
||||
<script>
|
||||
const PLANES=${planesJSON};
|
||||
const stack=document.getElementById('stack');
|
||||
const legend=document.getElementById('legend');
|
||||
const pills=document.getElementById('pills');
|
||||
const maxZ=Math.max(...PLANES.map(p=>p.z));
|
||||
const els={};
|
||||
legend.innerHTML='<b>planes</b> (front → back)<br>'+[...PLANES].reverse().map(p=>p.id+' · '+p.z+'mm').join('<br>');
|
||||
PLANES.forEach(p=>{
|
||||
const d=document.createElement('div');
|
||||
d.className='plane'; d.style.backgroundImage='url('+p.file+')';
|
||||
d.dataset.id=p.id; stack.appendChild(d); els[p.id]=d;
|
||||
const pill=document.createElement('span'); pill.className='pill'; pill.textContent=p.id;
|
||||
pill.onclick=()=>{d.dataset.off=d.dataset.off?'':'1'; pill.classList.toggle('off'); layout();};
|
||||
pills.appendChild(pill);
|
||||
});
|
||||
let gap=6, bright=1, mx=0, my=0;
|
||||
function layout(){
|
||||
for(const p of PLANES){
|
||||
const e=els[p.id];
|
||||
const z=(p.z-maxZ)*gap; // front plane at 0, others recede
|
||||
const blur=(p.blur||0)*(gap/6);
|
||||
const off=e.dataset.off?' opacity:0;':'';
|
||||
e.style.transform='translateZ('+z+'px)';
|
||||
e.style.filter='blur('+blur.toFixed(2)+'px)';
|
||||
e.style.opacity=e.dataset.off?0:(p.dim??1);
|
||||
e.style.mixBlendMode=p.blend||'normal';
|
||||
}
|
||||
document.getElementById('panel').style.filter='blur(2px) brightness('+bright+')';
|
||||
}
|
||||
function parallax(){
|
||||
stack.style.transform='rotateX('+(my*7)+'deg) rotateY('+(mx*9)+'deg)';
|
||||
}
|
||||
addEventListener('mousemove',e=>{
|
||||
mx=(e.clientX/innerWidth-0.5)*2; my=-(e.clientY/innerHeight-0.5)*2; parallax();
|
||||
});
|
||||
const gapI=document.getElementById('gap'), brI=document.getElementById('bright');
|
||||
gapI.oninput=()=>{gap=+gapI.value; document.getElementById('gv').textContent=' '+gap; layout();};
|
||||
brI.oninput=()=>{bright=+brI.value; document.getElementById('bv').textContent=' '+bright.toFixed(2); layout();};
|
||||
document.getElementById('gv').textContent=' '+gap;
|
||||
document.getElementById('bv').textContent=' '+bright.toFixed(2);
|
||||
layout(); parallax();
|
||||
</script></body></html>`);
|
||||
|
||||
console.log(`PROOF1 — ${W}×${H} (φ) — ${PLANES.length} planes → ${OUT}/`);
|
||||
for (const pl of PLANES) console.log(` ${pl.file.padEnd(26)} z=${pl.z}mm blur=${pl.blur} white=${pl.white}`);
|
||||
console.log(`manifest -> ${OUT}/manifest.json`);
|
||||
console.log(`simulator -> ${OUT}/simulator.html`);
|
||||
381
tools/refined-backlit.mjs
Normal file
@@ -0,0 +1,381 @@
|
||||
/* ============================================================
|
||||
refined-backlit.mjs — ONE SUN SETTING: five plates of a single
|
||||
seed's world (seethe-bold's sea, MESON-5113) across which time
|
||||
advances. The trigger event is byte-identical in every frame —
|
||||
a single millisecond, embalmed — while everything around it
|
||||
ages: the sun walks its setting diagonal L→R and sinks, the
|
||||
chamber keeps running (background events accumulate), the
|
||||
grain grows as the light dies, and THE WATER REMEMBERS.
|
||||
|
||||
The constitution (2026-07-05/06, from the maker):
|
||||
· THE WORLD IS SOVEREIGN IN THE INSTANT — the sea never reacts
|
||||
to the event within a frame; submerged evidence is refracted,
|
||||
tinted, dimmed, and the waves pass in front of it.
|
||||
· …BUT THE WATER REMEMBERS ACROSS EXPOSURES — frame by frame,
|
||||
the wave rows bend around where the evidence has been sitting
|
||||
(feDisplacementMap driven by the submerged ink, scale growing
|
||||
with time). Influence arrives only through time.
|
||||
· THE SKY IS THE MARGIN WHERE THE HAND LIVES — and the hand has
|
||||
a JOB: it tracks the descent. A pencil ghost-ring marks where
|
||||
the sun was in the PREVIOUS exposure; an altitude reading is
|
||||
logged each frame; when the sun sinks, the ring stops — the
|
||||
hand loses its subject and starts guessing (h −0·54?).
|
||||
· β-LAW COLOUR, magenta-centred — transparent births, the family
|
||||
holds hot magenta, violet is only the last word.
|
||||
· EMBER HEART · EVEN DEMOCRACY · AGED DOCUMENT (foxing, dust,
|
||||
scratches; grain ramps up as dusk falls).
|
||||
|
||||
Usage: node tools/refined-backlit.mjs [size]
|
||||
→ fable/REFINED_Backlit/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { splitTracksAtWaterline } from '../src/compose/waterline.js';
|
||||
import { chinagraphText } from '../src/scene/chinagraph.js';
|
||||
import { makeRng, gauss } from '../src/rng.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1500);
|
||||
const HAND = process.argv[3] !== undefined ? +process.argv[3] : 0; // scanner's-hand presence: 0 silent … 1 full (2026-07-16: DISABLED for now — still not feeling it; ledger experiment kept behind the flag)
|
||||
const OUT = process.argv[4] || 'fable/REFINED_Backlit';
|
||||
const LEDGER = process.argv[5] === 'ledger'; // EXPERIMENT 2026-07-16: embalmed marks — written once, persist on every later exposure
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = SIZE / 1000;
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- the world constants: seethe-bold's own config ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON = 0.37;
|
||||
const YH = HORIZON * 2 - 1; // -0.26 scene
|
||||
const FILM = { seed: 41, density: 0.6 };
|
||||
const GRAPHITE = '#39312a';
|
||||
|
||||
/* ---- film / grain / aging (the document's flesh) ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 6, dust = 0.5, foxing = 0.55, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * SIZE, y0 = rng() * SIZE * 0.4, len = (0.3 + rng() * 0.6) * SIZE;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${SIZE}" height="${SIZE}" filter="url(#fox)"/>
|
||||
<rect width="${SIZE}" height="${SIZE}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
function deck(c, warpFn) {
|
||||
const base = { mode: 'plate', rows: 46, horizon: HORIZON, wFar: 0.58, wNear: 0.7,
|
||||
overlap: 1.9, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56, blips: c.blips, warpFn };
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(SIZE, { ...base, salt: 'field' + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(c.chaos * 0.8, c.chaos, t),
|
||||
strokeNear: strokes[i].near, strokeFar: strokes[i].far });
|
||||
});
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
IRON FILINGS — the field attends to the evidence.
|
||||
A coarse attraction field is built from the submerged ink
|
||||
(track points + the sunken sun); every carpet row point is
|
||||
pulled TOWARD the nearest evidence, like filings onto a
|
||||
magnet. Two time-scales: a faint instantaneous pull in every
|
||||
frame (the field can never fully ignore the evidence) plus a
|
||||
memory ramp that deepens across the exposures.
|
||||
============================================================ */
|
||||
function buildAttraction(below, shock, sunSubmerged) {
|
||||
const S = (v) => SIZE / 2 + v * (SIZE / 2) * 0.98; // scene → px (renderSVG's mapping)
|
||||
const pts = [];
|
||||
for (const t of below) for (let i = 0; i < t.pts.length; i += 5)
|
||||
pts.push({ x: S(t.pts[i].x), y: S(t.pts[i].y), w: 1 });
|
||||
if (sunSubmerged) pts.push({ x: S(shock.x), y: S(shock.y), w: 12 });
|
||||
const G = 72, cell = SIZE / G, sig = 0.045 * SIZE;
|
||||
const fy = new Float32Array(G * G);
|
||||
for (let gy = 0; gy < G; gy++) for (let gx = 0; gx < G; gx++) {
|
||||
const x = (gx + 0.5) * cell, y = (gy + 0.5) * cell;
|
||||
let dy = 0, wsum = 0;
|
||||
for (const p of pts) {
|
||||
const dx0 = p.x - x, dy0 = p.y - y;
|
||||
const k = p.w * Math.exp(-(dx0 * dx0 + dy0 * dy0) / (2 * sig * sig));
|
||||
dy += k * Math.max(-sig, Math.min(sig, dy0));
|
||||
wsum += k;
|
||||
}
|
||||
fy[gy * G + gx] = wsum > 0.02 ? (dy / (wsum + 1.2)) / sig : 0; // → roughly -0.5..0.5, fades to 0 away from ink
|
||||
}
|
||||
return (x, y) => { // bilinear sample, px in → pull fraction out
|
||||
const fx0 = Math.min(G - 1.001, Math.max(0, x / cell - 0.5));
|
||||
const fy0 = Math.min(G - 1.001, Math.max(0, y / cell - 0.5));
|
||||
const ix = Math.floor(fx0), iy = Math.floor(fy0), ax = fx0 - ix, ay = fy0 - iy;
|
||||
const v00 = fy[iy * G + ix], v10 = fy[iy * G + ix + 1];
|
||||
const v01 = fy[(iy + 1) * G + ix], v11 = fy[(iy + 1) * G + ix + 1];
|
||||
return (v00 * (1 - ax) + v10 * ax) * (1 - ay) + (v01 * (1 - ax) + v11 * ax) * ay;
|
||||
};
|
||||
}
|
||||
// reconstruct carpetSVG's own row geometry so the pull is expressed in the
|
||||
// hook's normalized units (returned value is multiplied by the row's amp)
|
||||
function warpFor(sample, pullPx) {
|
||||
const hY = HORIZON * SIZE, bottom = 0.99 * SIZE;
|
||||
return (t, d) => {
|
||||
const half = (0.58 + 0.12 * d) * SIZE;
|
||||
const x = SIZE / 2 - half + t * 2 * half;
|
||||
const y = hY + (bottom - hY) * Math.pow(d, 1.7);
|
||||
const localGap = (bottom - hY) * 1.7 * Math.pow(Math.max(d, 0.02), 0.7) / 45;
|
||||
const amp = Math.max(2 * u, 1.9 * localGap);
|
||||
const n = sample(x, y) * pullPx / amp;
|
||||
return Math.max(-0.9, Math.min(0.9, n)); // pinch, don't shatter
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- the scanner's marks (beyond glyphs) ---- */
|
||||
function pencilRing(cx, cy, r, rng, ink, w) {
|
||||
const n = 44, start = rng() * Math.PI * 2, turn = Math.PI * 2 * (1.04 + rng() * 0.1);
|
||||
const dx = gauss(rng) * r * 0.05, dy = gauss(rng) * r * 0.05;
|
||||
let d = '';
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const a = start + turn * (i / n), rr = r * (1 + gauss(rng) * 0.045);
|
||||
d += `${i ? 'L' : 'M'} ${(cx + Math.cos(a) * rr + dx * i / n).toFixed(1)} ${(cy + Math.sin(a) * rr + dy * i / n).toFixed(1)} `;
|
||||
}
|
||||
return `<path d="${d}" fill="none" stroke="${ink}" stroke-opacity="0.6" stroke-width="${w.toFixed(2)}" stroke-linecap="round"/>`;
|
||||
}
|
||||
function pencilArc(cx, cy, r, a0, a1, rng, ink, w) {
|
||||
const n = 18; let d = '';
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const a = a0 + (a1 - a0) * (i / n);
|
||||
d += `${i ? 'L' : 'M'} ${(cx + Math.cos(a) * r + gauss(rng) * 1.4).toFixed(1)} ${(cy + Math.sin(a) * r + gauss(rng) * 1.4).toFixed(1)} `;
|
||||
}
|
||||
return `<path d="${d}" fill="none" stroke="${ink}" stroke-opacity="0.7" stroke-width="${w.toFixed(2)}" stroke-linecap="round"/>`;
|
||||
}
|
||||
|
||||
/* ---- the event ---- */
|
||||
function bcAssembleParams(over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 0.8,
|
||||
shockIntensity: 0.9, shockStriations: 1.0,
|
||||
diskHollow: +(process.env.HOLLOW ?? 0.85), // open the sun's heart: centre fades to paper, ember gathers in a ring
|
||||
depth: 0.3, aging: 0.25,
|
||||
}, over);
|
||||
return p;
|
||||
}
|
||||
|
||||
const fx = (sx) => ((sx + 1) / 2), fy = (sy) => ((sy + 1) / 2); // scene → frame fraction
|
||||
|
||||
function renderPlate(v, i) {
|
||||
const p = bcAssembleParams(v.bcOver);
|
||||
const scene = generateScene(p);
|
||||
const { above, below } = splitTracksAtWaterline(scene.tracks, YH, { kink: 0.016, compress: 0.95 });
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
|
||||
// RECIPROCITY (asymmetric): the submerged trails pick up a subtle waviness
|
||||
// along the field's rows — coerced, but far less than they coerce.
|
||||
const wamp = v.wave;
|
||||
const belowWavy = below.map(t => ({
|
||||
...t,
|
||||
pts: t.pts.map(pt => ({ ...pt, y: pt.y + wamp * Math.sin(pt.x * 34 + pt.y * 18) })),
|
||||
}));
|
||||
|
||||
const trackSVG = (tracks) => renderSVG({ ...bare, tracks, shock: null }, { ...p, emit: ['bubble'] }, SIZE);
|
||||
const diskSVG = () => renderSVG({ ...bare, tracks: [], shock: scene.shock }, { ...p, emit: ['disk'] }, SIZE);
|
||||
|
||||
const aboveImg = dataUri(trackSVG(above));
|
||||
const belowImg = dataUri(trackSVG(belowWavy));
|
||||
const diskImg = dataUri(diskSVG());
|
||||
|
||||
// IRON FILINGS: every wave row is pulled toward the submerged evidence.
|
||||
const sunSubmerged = scene.shock && scene.shock.y > YH - scene.shock.r * 0.4;
|
||||
const sample = buildAttraction(belowWavy, scene.shock, sunSubmerged);
|
||||
const sheets = deck(v.sea, warpFor(sample, v.pull * u)).map(dataUri);
|
||||
|
||||
const film = dataUri(filmSVG(FILM));
|
||||
const grain = dataUri(grainSVG({ amount: v.grain }));
|
||||
const aging = dataUri(agingSVG({ seed: 5 + i, scratches: 6, dust: 0.5, foxing: 0.55 }));
|
||||
|
||||
const hpx = (HORIZON * SIZE).toFixed(1);
|
||||
const m = v.muffle;
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
|
||||
|
||||
/* ---- the hand: tracking the descent ---- */
|
||||
const hRng = makeRng(SEED, 'hand:' + v.name);
|
||||
const px = (f) => f * SIZE, py = (f) => f * SIZE;
|
||||
let hand = '';
|
||||
if (HAND > 0 && LEDGER) {
|
||||
/* EMBALMED MARKS — one physical plate, five exposures. Each mark is
|
||||
written ONCE, at its own exposure (rng seeded per mark, not per
|
||||
plate), so its strokes are byte-identical on every later plate.
|
||||
The ledger accumulates in the corner; the rings march down the
|
||||
diagonal and stop where the water takes their subject. */
|
||||
const rngFor = (tag) => makeRng(SEED, 'mark:' + tag);
|
||||
const LX = 0.735; // ledger lives top-right: the north-west sky belongs to the sun's history
|
||||
hand += chinagraphText('Nº 217', { x: px(LX), y: py(0.06), h: 20 * u, rng: rngFor('label'), ink: GRAPHITE, width: 2.6 * u });
|
||||
for (let k = 0; k <= i; k++) {
|
||||
const line = PLATES[k].time + ' h ' + alt(k) + (k === PLATES.length - 1 ? '?' : '');
|
||||
hand += chinagraphText(line, { x: px(LX), y: py(0.102 + k * 0.031), h: 15 * u, rng: rngFor('line' + k), ink: GRAPHITE, width: 2.0 * u });
|
||||
if (k < i && SUNPATH[k][1] < YH - 0.02)
|
||||
hand += `<g clip-path="url(#skyc)">${pencilRing(px(fx(SUNPATH[k][0])), py(fy(SUNPATH[k][1])), 0.05 * SIZE, rngFor('ring' + k), GRAPHITE, 2.2 * u)}</g>`;
|
||||
}
|
||||
if (i >= 2) { // the θ measurement, taken at the third exposure, persists
|
||||
const a = PLATES[2].arc;
|
||||
hand += pencilArc(px(a.at[0]), py(a.at[1]), a.r * SIZE, a.a0, a.a1, rngFor('arc'), GRAPHITE, 2.2 * u);
|
||||
hand += chinagraphText(a.text, { x: px(a.textAt[0]), y: py(a.textAt[1]), h: 17 * u, rng: rngFor('arctext'), ink: GRAPHITE, width: 2.2 * u });
|
||||
}
|
||||
} else if (HAND > 0) {
|
||||
if (v.label) hand += chinagraphText(v.label, { x: px(0.065), y: py(0.085), h: 26 * u, rng: hRng, ink: GRAPHITE, width: 3.0 * u });
|
||||
hand += chinagraphText(v.time, { x: px(0.065), y: py(0.125), h: 21 * u, rng: hRng, ink: GRAPHITE, width: 2.6 * u });
|
||||
// ghost-ring where the sun was LAST exposure (only while that spot is in the sky)
|
||||
if (v.ghost) {
|
||||
const [gx, gy, gr] = v.ghost;
|
||||
hand += `<g clip-path="url(#skyc)">${pencilRing(px(gx), py(gy), gr * SIZE, hRng, GRAPHITE, 2.4 * u)}</g>`;
|
||||
}
|
||||
// altitude reading, logged beside the sun's vertical
|
||||
if (v.alt) hand += chinagraphText(v.alt.text, { x: px(v.alt.at[0]), y: py(v.alt.at[1]), h: 19 * u, rng: hRng, ink: GRAPHITE, width: 2.4 * u });
|
||||
// plate 3: the measured angle lives ON the vertex, arc + reading
|
||||
if (v.arc) {
|
||||
const [ax, ay] = v.arc.at;
|
||||
hand += pencilArc(px(ax), py(ay), v.arc.r * SIZE, v.arc.a0, v.arc.a1, hRng, GRAPHITE, 2.2 * u);
|
||||
hand += chinagraphText(v.arc.text, { x: px(v.arc.textAt[0]), y: py(v.arc.textAt[1]), h: 19 * u, rng: hRng, ink: GRAPHITE, width: 2.4 * u });
|
||||
}
|
||||
}
|
||||
if (HAND > 0 && HAND < 1) hand = `<g opacity="${HAND}">${hand}</g>`; // the hand recedes into the wax
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subT" x="-6%" y="-6%" width="112%" height="112%">
|
||||
<feColorMatrix type="matrix" values="0.78 0 0 0 0.010 0 0.94 0 0 0.022 0 0 0.96 0 0.022 0 0 0 0.85 0"/>
|
||||
<feGaussianBlur stdDeviation="${(1.0 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 ${(1 - 0.55 * m).toFixed(2)} 0"/>
|
||||
<feGaussianBlur stdDeviation="${((0.6 + 3.2 * m) * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${SIZE}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${SIZE}" height="${(SIZE - HORIZON * SIZE).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${SIZE}" height="${SIZE}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg)}</g>
|
||||
${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)"')}</g>
|
||||
${IMG(belowImg, 'filter="url(#subT)"')}
|
||||
<rect x="0" y="${hpx}" width="${SIZE}" height="${SIZE}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="0.72"`)}
|
||||
${IMG(sheets[2], 'opacity="0.95"')}
|
||||
${IMG(aboveImg)}
|
||||
${hand}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, `opacity="0.45" style="mix-blend-mode:multiply"`)}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} sun(${v.bcOver.shockX},${v.bcOver.shockY}) bg=${v.bcOver.bgEvents} pull=${v.pull}`);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
ONE SUN SETTING — five exposures of plate Nº 217.
|
||||
The trigger is identical in every frame (embalmed); the sun
|
||||
walks its setting diagonal; the chamber keeps running; the
|
||||
grain grows; the water remembers.
|
||||
============================================================ */
|
||||
const EVENT = { eventX: -0.14, eventY: -0.37 };
|
||||
const TRACES = { sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8 };
|
||||
const SUNPATH = [[-0.55, -0.72], [-0.37, -0.53], [0.02, YH], [0.22, 0.15], [0.4, 0.27]];
|
||||
const alt = (i) => {
|
||||
const h = YH - SUNPATH[i][1];
|
||||
const s = Math.abs(h).toFixed(2).replace('0.', '0·');
|
||||
return (h > 0.005 ? '+' : h < -0.005 ? '-' : '') + s;
|
||||
};
|
||||
|
||||
const PLATES = [
|
||||
{ name: '01_sun-above', time: '19:04', label: 'Nº 217',
|
||||
muffle: 0, pull: 7, wave: 0.003, grain: 0.42,
|
||||
sea: { chaos: 0.8, blips: 1.4 },
|
||||
alt: { text: 'h ' + alt(0), at: [fx(SUNPATH[0][0]) + 0.1, fy(SUNPATH[0][1]) - 0.02] },
|
||||
bcOver: { ...EVENT, ...TRACES, bgEvents: 2, aging: 0.2, shockX: SUNPATH[0][0], shockY: SUNPATH[0][1], shockSize: 0.17, shockIntensity: 0.8 } },
|
||||
|
||||
{ name: '02_sun-touching', time: '19:26',
|
||||
muffle: 0.18, pull: 13, wave: 0.0045, grain: 0.46,
|
||||
sea: { chaos: 0.84, blips: 1.2 },
|
||||
ghost: [fx(SUNPATH[0][0]), fy(SUNPATH[0][1]), 0.055],
|
||||
alt: { text: 'h ' + alt(1), at: [fx(SUNPATH[1][0]) + 0.11, fy(SUNPATH[1][1]) - 0.02] },
|
||||
bcOver: { ...EVENT, ...TRACES, bgEvents: 4, aging: 0.3, shockX: SUNPATH[1][0], shockY: SUNPATH[1][1], shockSize: 0.19, shockIntensity: 0.85 } },
|
||||
|
||||
{ name: '03_sun-half', time: '19:47',
|
||||
muffle: 0.4, pull: 20, wave: 0.006, grain: 0.5,
|
||||
sea: { chaos: 0.88, blips: 1.0 },
|
||||
ghost: [fx(SUNPATH[1][0]), fy(SUNPATH[1][1]), 0.06],
|
||||
alt: { text: 'h 0·00', at: [fx(SUNPATH[2][0]) + 0.12, fy(YH) - 0.035] },
|
||||
arc: { at: [fx(EVENT.eventX), fy(EVENT.eventY)], r: 0.055, a0: 2.4, a1: 3.4, text: 'θ 38°',
|
||||
textAt: [fx(EVENT.eventX) - 0.185, fy(EVENT.eventY) + 0.045] },
|
||||
bcOver: { ...EVENT, ...TRACES, bgEvents: 6, aging: 0.45, shockX: SUNPATH[2][0], shockY: SUNPATH[2][1], shockSize: 0.21 } },
|
||||
|
||||
{ name: '04_sun-drowned', time: '20:08', label: 'Nº 217',
|
||||
muffle: 0.62, pull: 28, wave: 0.008, grain: 0.55,
|
||||
sea: { chaos: 0.92, blips: 0.85 },
|
||||
// no ghost-ring: the previous position is at the waterline — the hand
|
||||
// cannot ring what the water has taken; only the reading continues
|
||||
alt: { text: 'h ' + alt(3), at: [fx(SUNPATH[3][0]) + 0.1, fy(YH) - 0.03] },
|
||||
bcOver: { ...EVENT, ...TRACES, bgEvents: 8, aging: 0.6, shockX: SUNPATH[3][0], shockY: SUNPATH[3][1], shockSize: 0.24 } },
|
||||
|
||||
{ name: '05_sun-gone', time: '20:31',
|
||||
muffle: 0.85, pull: 36, wave: 0.01, grain: 0.6,
|
||||
sea: { chaos: 0.96, blips: 0.7 },
|
||||
alt: { text: 'h ' + alt(4) + '?', at: [fx(SUNPATH[4][0]) + 0.06, fy(YH) - 0.03] },
|
||||
bcOver: { ...EVENT, ...TRACES, bgEvents: 10, aging: 0.75, shockX: SUNPATH[4][0], shockY: SUNPATH[4][1], shockSize: 0.27, shockStriations: 1.2 } },
|
||||
];
|
||||
|
||||
console.log(`ONE SUN SETTING — ${PLATES.length} plates → ${OUT}/`);
|
||||
PLATES.forEach(renderPlate);
|
||||
|
||||
const cap = (v) => v.name.replace(/^\d+_/, '').replace(/-/g, ' ');
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit · one sun setting</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}
|
||||
.grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:22px}
|
||||
.notes{color:#999;background:#161210;padding:12px 16px;border-left:3px solid #d9b48a;margin:14px 0;max-width:960px}</style></head><body>
|
||||
<h1>One sun setting — plate Nº 217, five exposures</h1>
|
||||
<div class="notes">The trigger event is identical in every frame — a single millisecond, embalmed — while everything around it ages: the sun walks its setting diagonal and sinks, the chamber keeps running (history accumulates), the grain grows as the light dies, and the water remembers: the wave rows slowly bend around the submerged evidence. The sea never reacts in the instant; influence arrives only through time. The hand tracks the descent — ghost-rings where the sun was, altitude readings, and when the sun is gone, a guess.</div>
|
||||
<div class="row">${PLATES.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)} · ${v.time}</figcaption></figure>`).join('')}</div>
|
||||
<div class="grid">${PLATES.map(v => `<figure><img src="${v.name}.svg"><figcaption>${cap(v)} · ${v.time}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||
255
tools/refined-backlit2.mjs
Normal file
@@ -0,0 +1,255 @@
|
||||
/* ============================================================
|
||||
refined-backlit2.mjs — SUN-ABOVE SCALE STUDIES.
|
||||
One frame (plate Nº 217, exposure 19:04, MESON-5113), colours
|
||||
and composition held constant; each render plays with the
|
||||
RELATIVE SCALE of the marks:
|
||||
· traces — bubble size / density of the particle ink
|
||||
· waves — row count (few = broad swells, many = fine ripple)
|
||||
· depth — overlap: wave amplitude relative to row gap
|
||||
· line — stroke weight of the wave rows
|
||||
No hand. Same sun (hollow heart), same sea hue, same aging.
|
||||
|
||||
Usage: node tools/refined-backlit2.mjs [size]
|
||||
→ fable/REFINED_Backlit2/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { splitTracksAtWaterline } from '../src/compose/waterline.js';
|
||||
import { makeRng } from '../src/rng.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1500);
|
||||
const OUT = 'fable/REFINED_Backlit2';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = SIZE / 1000;
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- the world constants: identical to One Sun Setting ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON = 0.37;
|
||||
const YH = HORIZON * 2 - 1;
|
||||
const FILM = { seed: 41, density: 0.6 };
|
||||
|
||||
/* ---- film / grain / aging (unchanged from refined-backlit) ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 6, dust = 0.5, foxing = 0.55, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * SIZE, y0 = rng() * SIZE * 0.4, len = (0.3 + rng() * 0.6) * SIZE;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${SIZE}" height="${SIZE}" filter="url(#fox)"/>
|
||||
<rect width="${SIZE}" height="${SIZE}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- the sea deck, scale-parameterised ----
|
||||
rows: wave count · overlap: amplitude vs row gap (depth) ·
|
||||
strokeMul: line weight multiplier on the per-sheet stroke table */
|
||||
function deck(sea, warpFn) {
|
||||
const rows = sea.rows ?? 46, overlap = sea.overlap ?? 1.9, sm = sea.strokeMul ?? 1;
|
||||
const base = { mode: 'plate', rows, horizon: HORIZON, wFar: 0.58, wNear: 0.7,
|
||||
overlap, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56, blips: 1.4, warpFn };
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(SIZE, { ...base, salt: 'field' + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(0.8 * 0.8, 0.8, t),
|
||||
strokeNear: strokes[i].near * sm, strokeFar: strokes[i].far * sm });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- iron filings (identical logic, geometry follows the sea's scale) ---- */
|
||||
function buildAttraction(below, shock, sunSubmerged) {
|
||||
const S = (v) => SIZE / 2 + v * (SIZE / 2) * 0.98;
|
||||
const pts = [];
|
||||
for (const t of below) for (let i = 0; i < t.pts.length; i += 5)
|
||||
pts.push({ x: S(t.pts[i].x), y: S(t.pts[i].y), w: 1 });
|
||||
if (sunSubmerged) pts.push({ x: S(shock.x), y: S(shock.y), w: 12 });
|
||||
const G = 72, cell = SIZE / G, sig = 0.045 * SIZE;
|
||||
const fy = new Float32Array(G * G);
|
||||
for (let gy = 0; gy < G; gy++) for (let gx = 0; gx < G; gx++) {
|
||||
const x = (gx + 0.5) * cell, y = (gy + 0.5) * cell;
|
||||
let dy = 0, wsum = 0;
|
||||
for (const p of pts) {
|
||||
const dx0 = p.x - x, dy0 = p.y - y;
|
||||
const k = p.w * Math.exp(-(dx0 * dx0 + dy0 * dy0) / (2 * sig * sig));
|
||||
dy += k * Math.max(-sig, Math.min(sig, dy0));
|
||||
wsum += k;
|
||||
}
|
||||
fy[gy * G + gx] = wsum > 0.02 ? (dy / (wsum + 1.2)) / sig : 0;
|
||||
}
|
||||
return (x, y) => {
|
||||
const fx0 = Math.min(G - 1.001, Math.max(0, x / cell - 0.5));
|
||||
const fy0 = Math.min(G - 1.001, Math.max(0, y / cell - 0.5));
|
||||
const ix = Math.floor(fx0), iy = Math.floor(fy0), ax = fx0 - ix, ay = fy0 - iy;
|
||||
const v00 = fy[iy * G + ix], v10 = fy[iy * G + ix + 1];
|
||||
const v01 = fy[(iy + 1) * G + ix], v11 = fy[(iy + 1) * G + ix + 1];
|
||||
return (v00 * (1 - ax) + v10 * ax) * (1 - ay) + (v01 * (1 - ax) + v11 * ax) * ay;
|
||||
};
|
||||
}
|
||||
function warpFor(sample, pullPx, sea) {
|
||||
const rows = sea.rows ?? 46, overlap = sea.overlap ?? 1.9;
|
||||
const hY = HORIZON * SIZE, bottom = 0.99 * SIZE;
|
||||
return (t, d) => {
|
||||
const half = (0.58 + 0.12 * d) * SIZE;
|
||||
const x = SIZE / 2 - half + t * 2 * half;
|
||||
const y = hY + (bottom - hY) * Math.pow(d, 1.7);
|
||||
const localGap = (bottom - hY) * 1.7 * Math.pow(Math.max(d, 0.02), 0.7) / (rows - 1);
|
||||
const amp = Math.max(2 * u, overlap * localGap);
|
||||
const n = sample(x, y) * pullPx / amp;
|
||||
return Math.max(-0.9, Math.min(0.9, n));
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- the event: sun-above config, colours fixed ---- */
|
||||
function bcAssembleParams(over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 0.8,
|
||||
shockStriations: 1.0, diskHollow: 0.85,
|
||||
depth: 0.3, aging: 0.2,
|
||||
eventX: -0.14, eventY: -0.37,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2, shockX: -0.55, shockY: -0.72, shockSize: 0.17, shockIntensity: 0.8,
|
||||
}, over);
|
||||
return p;
|
||||
}
|
||||
|
||||
function renderVariant(v) {
|
||||
const p = bcAssembleParams(v.traces || {});
|
||||
const scene = generateScene(p);
|
||||
const { above, below } = splitTracksAtWaterline(scene.tracks, YH, { kink: 0.016, compress: 0.95 });
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
const sea = v.sea || {};
|
||||
|
||||
const wamp = 0.003;
|
||||
const belowWavy = below.map(t => ({
|
||||
...t,
|
||||
pts: t.pts.map(pt => ({ ...pt, y: pt.y + wamp * Math.sin(pt.x * 34 + pt.y * 18) })),
|
||||
}));
|
||||
|
||||
const trackSVG = (tracks) => renderSVG({ ...bare, tracks, shock: null }, { ...p, emit: ['bubble'] }, SIZE);
|
||||
const diskSVG = () => renderSVG({ ...bare, tracks: [], shock: scene.shock }, { ...p, emit: ['disk'] }, SIZE);
|
||||
|
||||
const aboveImg = dataUri(trackSVG(above));
|
||||
const belowImg = dataUri(trackSVG(belowWavy));
|
||||
const diskImg = dataUri(diskSVG());
|
||||
|
||||
const sample = buildAttraction(belowWavy, scene.shock, false);
|
||||
const sheets = deck(sea, warpFor(sample, 7 * u, sea)).map(dataUri);
|
||||
|
||||
const film = dataUri(filmSVG(FILM));
|
||||
const grain = dataUri(grainSVG({ amount: 0.42 }));
|
||||
const aging = dataUri(agingSVG({ seed: 5, scratches: 6, dust: 0.5, foxing: 0.55 }));
|
||||
|
||||
const hpx = (HORIZON * SIZE).toFixed(1);
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subT" x="-6%" y="-6%" width="112%" height="112%">
|
||||
<feColorMatrix type="matrix" values="0.78 0 0 0 0.010 0 0.94 0 0 0.022 0 0 0.96 0 0.022 0 0 0 0.85 0"/>
|
||||
<feGaussianBlur stdDeviation="${(1.0 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
||||
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${SIZE}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${SIZE}" height="${(SIZE - HORIZON * SIZE).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${SIZE}" height="${SIZE}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg)}</g>
|
||||
${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)"')}</g>
|
||||
${IMG(belowImg, 'filter="url(#subT)"')}
|
||||
<rect x="0" y="${hpx}" width="${SIZE}" height="${SIZE}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="0.72"`)}
|
||||
${IMG(sheets[2], 'opacity="0.95"')}
|
||||
${IMG(aboveImg)}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, `opacity="0.45" style="mix-blend-mode:multiply"`)}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} ${v.note}`);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
THE STUDIES — single axes first, then two deliberate chords.
|
||||
============================================================ */
|
||||
const VARIANTS = [
|
||||
{ name: '00_reference', note: 'baseline (traces 1.0 · rows 46 · overlap 1.9 · line ×1)',
|
||||
traces: {}, sea: {} },
|
||||
{ name: '01_traces-bold', note: 'evidence enlarged (bubble size 1.6, density 0.9)',
|
||||
traces: { size: 1.6, density: 0.9 } },
|
||||
{ name: '02_traces-fine', note: 'evidence miniaturised (size 0.62, density 1.3)',
|
||||
traces: { size: 0.62, density: 1.3 } },
|
||||
{ name: '03_sea-broad', note: 'few broad swells (rows 26)',
|
||||
sea: { rows: 26 } },
|
||||
{ name: '04_sea-fine', note: 'fine ripple (rows 78, line ×0.75)',
|
||||
sea: { rows: 78, strokeMul: 0.75 } },
|
||||
{ name: '05_sea-deep', note: 'deep undulation (overlap 3.1)',
|
||||
sea: { overlap: 3.1 } },
|
||||
{ name: '06_sea-etched', note: 'heavy wave line (line ×2.3)',
|
||||
sea: { strokeMul: 2.3 } },
|
||||
{ name: '07_giant-evidence', note: 'large ink over fine calm water (size 1.7 · rows 78 · line ×0.7 · overlap 1.6)',
|
||||
traces: { size: 1.7, density: 0.85 }, sea: { rows: 78, strokeMul: 0.7, overlap: 1.6 } },
|
||||
{ name: '08_woodcut', note: 'small ink in a heavy carved sea (size 0.7 · rows 24 · line ×2.4 · overlap 2.6)',
|
||||
traces: { size: 0.7, density: 1.3 }, sea: { rows: 24, strokeMul: 2.4, overlap: 2.6 } },
|
||||
];
|
||||
|
||||
console.log(`SUN-ABOVE SCALE STUDIES — ${VARIANTS.length} renders → ${OUT}/`);
|
||||
VARIANTS.forEach(renderVariant);
|
||||
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit2 · sun-above scale studies</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>Sun-above — scale studies (colours & composition held)</h1>
|
||||
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name.replace(/^\d+_/, '')} — ${v.note}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||
260
tools/refined-backlit3.mjs
Normal file
@@ -0,0 +1,260 @@
|
||||
/* ============================================================
|
||||
refined-backlit3.mjs — SUN-ABOVE, BOLDER · LARGE TRACES.
|
||||
One frame (plate Nº 217, exposure 19:04, MESON-5113). This
|
||||
generation commits to LARGER bubble traces throughout and a
|
||||
generally BOLDER read; each render plays the waves differently
|
||||
(count / depth / line weight / chaos). The pressure disc is
|
||||
FADED INTO THE BACK — its heart fully hollowed and the whole
|
||||
sun dimmed + pushed behind the sea, a ghost the tracks cross.
|
||||
Film grain + distressing (foxing / dust / scratches) stay ON.
|
||||
|
||||
Usage: node tools/refined-backlit3.mjs [size]
|
||||
→ fable/REFINED_Backlit3/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { makeRng } from '../src/rng.js';
|
||||
|
||||
const SIZE = +(process.argv[2] || 1500);
|
||||
const OUT = 'fable/REFINED_Backlit3';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = SIZE / 1000;
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- the world constants: identical to One Sun Setting ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON = 0.37;
|
||||
const YH = HORIZON * 2 - 1;
|
||||
const FILM = { seed: 41, density: 0.6 };
|
||||
|
||||
/* ---- film / grain / aging (unchanged from refined-backlit) ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${SIZE}" height="${SIZE}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 6, dust = 0.5, foxing = 0.55, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * SIZE, y0 = rng() * SIZE * 0.4, len = (0.3 + rng() * 0.6) * SIZE;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${SIZE}" height="${SIZE}" filter="url(#fox)"/>
|
||||
<rect width="${SIZE}" height="${SIZE}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- the sea deck, scale-parameterised ----
|
||||
rows: wave count · overlap: amplitude vs row gap (depth) ·
|
||||
strokeMul: line weight multiplier on the per-sheet stroke table */
|
||||
function deck(sea, warpFn) {
|
||||
const rows = sea.rows ?? 40, overlap = sea.overlap ?? 2.2, sm = sea.strokeMul ?? 1.35; // v3: bolder default line
|
||||
const chaosMax = sea.chaos ?? 0.8, blips = sea.blips ?? 1.4;
|
||||
const base = { mode: 'plate', rows, horizon: HORIZON, wFar: 0.58, wNear: 0.7,
|
||||
overlap, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56, blips, warpFn };
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(SIZE, { ...base, salt: 'field' + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(chaosMax * 0.8, chaosMax, t),
|
||||
strokeNear: strokes[i].near * sm, strokeFar: strokes[i].far * sm });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- iron filings (identical logic, geometry follows the sea's scale) ---- */
|
||||
function buildAttraction(below, shock, sunSubmerged) {
|
||||
const S = (v) => SIZE / 2 + v * (SIZE / 2) * 0.98;
|
||||
const pts = [];
|
||||
for (const t of below) for (let i = 0; i < t.pts.length; i += 5)
|
||||
pts.push({ x: S(t.pts[i].x), y: S(t.pts[i].y), w: 1 });
|
||||
if (sunSubmerged) pts.push({ x: S(shock.x), y: S(shock.y), w: 12 });
|
||||
const G = 72, cell = SIZE / G, sig = 0.045 * SIZE;
|
||||
const fy = new Float32Array(G * G);
|
||||
for (let gy = 0; gy < G; gy++) for (let gx = 0; gx < G; gx++) {
|
||||
const x = (gx + 0.5) * cell, y = (gy + 0.5) * cell;
|
||||
let dy = 0, wsum = 0;
|
||||
for (const p of pts) {
|
||||
const dx0 = p.x - x, dy0 = p.y - y;
|
||||
const k = p.w * Math.exp(-(dx0 * dx0 + dy0 * dy0) / (2 * sig * sig));
|
||||
dy += k * Math.max(-sig, Math.min(sig, dy0));
|
||||
wsum += k;
|
||||
}
|
||||
fy[gy * G + gx] = wsum > 0.02 ? (dy / (wsum + 1.2)) / sig : 0;
|
||||
}
|
||||
return (x, y) => {
|
||||
const fx0 = Math.min(G - 1.001, Math.max(0, x / cell - 0.5));
|
||||
const fy0 = Math.min(G - 1.001, Math.max(0, y / cell - 0.5));
|
||||
const ix = Math.floor(fx0), iy = Math.floor(fy0), ax = fx0 - ix, ay = fy0 - iy;
|
||||
const v00 = fy[iy * G + ix], v10 = fy[iy * G + ix + 1];
|
||||
const v01 = fy[(iy + 1) * G + ix], v11 = fy[(iy + 1) * G + ix + 1];
|
||||
return (v00 * (1 - ax) + v10 * ax) * (1 - ay) + (v01 * (1 - ax) + v11 * ax) * ay;
|
||||
};
|
||||
}
|
||||
function warpFor(sample, pullPx, sea) {
|
||||
const rows = sea.rows ?? 40, overlap = sea.overlap ?? 2.2;
|
||||
const hY = HORIZON * SIZE, bottom = 0.99 * SIZE;
|
||||
return (t, d) => {
|
||||
const half = (0.58 + 0.12 * d) * SIZE;
|
||||
const x = SIZE / 2 - half + t * 2 * half;
|
||||
const y = hY + (bottom - hY) * Math.pow(d, 1.7);
|
||||
const localGap = (bottom - hY) * 1.7 * Math.pow(Math.max(d, 0.02), 0.7) / (rows - 1);
|
||||
const amp = Math.max(2 * u, overlap * localGap);
|
||||
const n = sample(x, y) * pullPx / amp;
|
||||
return Math.max(-0.9, Math.min(0.9, n));
|
||||
};
|
||||
}
|
||||
|
||||
/* ---- the event: sun-above config, colours fixed ---- */
|
||||
// seed drives ONLY the stochastic realization; the MESON-5113 parameter
|
||||
// fingerprint (event position, counts, physics, sun) is held constant, so a
|
||||
// new seed re-rolls the bubble layout without changing the piece's character.
|
||||
function bcAssembleParams(over = {}, seed = SEED) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4, // softer → the disc reads as a haze behind, not an object
|
||||
shockStriations: 1.0, diskHollow: 1.0, // heart fully open: the sun is a ring of light, not a plug
|
||||
depth: 0.3, aging: 0.28, // distressing up a touch (bolder document)
|
||||
eventX: -0.14, eventY: -0.37,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2, shockX: -0.55, shockY: -0.72, shockSize: 0.17, shockIntensity: 0.8,
|
||||
}, over, { seed });
|
||||
return p;
|
||||
}
|
||||
|
||||
/* ---- the SUN and the WAVE-FIELD are fixed across the whole set (base seed),
|
||||
so the sea is byte-identical per iteration and only the ink is re-rolled ---- */
|
||||
const baseP = bcAssembleParams();
|
||||
const baseScene = generateScene(baseP);
|
||||
const baseBare = { ...baseScene, instrument: null, artifacts: null, media: null };
|
||||
const diskImg = dataUri(renderSVG({ ...baseBare, tracks: [], shock: baseScene.shock }, { ...baseP, emit: ['disk'] }, SIZE));
|
||||
const baseBelow = baseScene.tracks
|
||||
.map(t => ({ ...t, pts: t.pts.filter(pt => pt.y > YH) }))
|
||||
.filter(t => t.pts.length);
|
||||
const fieldSample = buildAttraction(baseBelow, baseScene.shock, false); // waves attend to the base event only
|
||||
|
||||
function renderVariant(v, i) {
|
||||
// unique bubble-trace seed per iteration; waves + sun stay put
|
||||
const p = bcAssembleParams(v.traces || {}, `${SEED}#${String(i).padStart(2, '0')}`);
|
||||
const scene = generateScene(p);
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
const sea = v.sea || {};
|
||||
|
||||
// REVERTED 2026-07-17: no waterline split, no refraction — the traces are ONE
|
||||
// object drawn cleanly ON TOP of the sea; the water neither fronts nor tints them.
|
||||
const tracksImg = dataUri(renderSVG({ ...bare, tracks: scene.tracks, shock: null }, { ...p, emit: ['bubble'] }, SIZE));
|
||||
|
||||
const sheets = deck(sea, warpFor(fieldSample, 7 * u, sea)).map(dataUri);
|
||||
|
||||
const film = dataUri(filmSVG(FILM));
|
||||
const grain = dataUri(grainSVG({ amount: 0.52 })); // bolder film grain
|
||||
const aging = dataUri(agingSVG({ seed: 5, scratches: 8, dust: 0.6, foxing: 0.62 })); // heavier distressing
|
||||
|
||||
const hpx = (HORIZON * SIZE).toFixed(1);
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${SIZE}" height="${SIZE}" href="${href}" ${attrs}/>`;
|
||||
const diskFade = v.diskFade ?? 0.5; // the sun receded into the back: a dim haze the evidence crosses
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
||||
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="diskback" x="-12%" y="-12%" width="124%" height="124%"><feGaussianBlur stdDeviation="${(2.2 * u).toFixed(2)}"/></filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${SIZE}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${SIZE}" height="${(SIZE - HORIZON * SIZE).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${SIZE}" height="${SIZE}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg, `filter="url(#diskback)" opacity="${diskFade.toFixed(2)}"`)}</g>
|
||||
${IMG(sheets[0], 'filter="url(#b3)" opacity="0.5"')}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, `filter="url(#subD)" opacity="${(0.7 + 0.3 * diskFade).toFixed(2)}"`)}</g>
|
||||
<rect x="0" y="${hpx}" width="${SIZE}" height="${SIZE}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="0.72"`)}
|
||||
${IMG(sheets[2], 'opacity="0.95"')}
|
||||
${IMG(tracksImg)}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, `opacity="0.45" style="mix-blend-mode:multiply"`)}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} ${v.note}`);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
THE STUDIES — single axes first, then two deliberate chords.
|
||||
============================================================ */
|
||||
// Every variant carries LARGE bubble traces (evidence on top of the sea) and a
|
||||
// generally bolder read; the sun is faded into the back. Each render moves the
|
||||
// WAVES somewhere different — count, depth, line weight, chaos.
|
||||
const VARIANTS = [
|
||||
{ name: '00_bold-base', note: 'large ink · sea baseline (rows 40 · overlap 2.2 · line ×1.35)',
|
||||
traces: { size: 1.5, density: 1.0 }, sea: {}, diskFade: 0.5 },
|
||||
{ name: '01_broad-swell', note: 'few broad swells (rows 24 · overlap 2.6)',
|
||||
traces: { size: 1.55, density: 1.0 }, sea: { rows: 24, overlap: 2.6 }, diskFade: 0.5 },
|
||||
{ name: '02_fine-ripple', note: 'dense fine ripple (rows 82 · line ×0.9)',
|
||||
traces: { size: 1.5, density: 1.0 }, sea: { rows: 82, strokeMul: 0.9 }, diskFade: 0.5 },
|
||||
{ name: '03_deep-heave', note: 'deep undulation (overlap 3.4)',
|
||||
traces: { size: 1.55, density: 1.0 }, sea: { overlap: 3.4 }, diskFade: 0.45 },
|
||||
{ name: '04_etched-sea', note: 'heavy carved wave line (line ×2.6)',
|
||||
traces: { size: 1.5, density: 1.05 }, sea: { strokeMul: 2.6 }, diskFade: 0.5 },
|
||||
{ name: '05_glassy-calm', note: 'shallow glassy water (overlap 1.3 · rows 52)',
|
||||
traces: { size: 1.6, density: 0.95 }, sea: { overlap: 1.3, rows: 52 }, diskFade: 0.55 },
|
||||
{ name: '06_turbulent', note: 'turbulent, blip-rich sea (chaos 1.0 · blips 2.2 · overlap 2.8)',
|
||||
traces: { size: 1.5, density: 1.05 }, sea: { chaos: 1.0, blips: 2.2, overlap: 2.8 }, diskFade: 0.45 },
|
||||
{ name: '07_woodcut', note: 'heavy carved swell (rows 26 · line ×2.8 · overlap 3.0)',
|
||||
traces: { size: 1.55, density: 1.0 }, sea: { rows: 26, strokeMul: 2.8, overlap: 3.0 }, diskFade: 0.5 },
|
||||
{ name: '08_giant-ink-calm', note: 'giant evidence over quiet fine water (size 1.9 · rows 78 · line ×0.8 · overlap 1.5)',
|
||||
traces: { size: 1.9, density: 0.9 }, sea: { rows: 78, strokeMul: 0.8, overlap: 1.5 }, diskFade: 0.55 },
|
||||
{ name: '09_max-bold', note: 'everything loud (size 1.8 · rows 32 · line ×2.4 · overlap 3.0 · chaos 0.95)',
|
||||
traces: { size: 1.8, density: 1.1 }, sea: { rows: 32, strokeMul: 2.4, overlap: 3.0, chaos: 0.95, blips: 1.8 }, diskFade: 0.4 },
|
||||
];
|
||||
|
||||
console.log(`SUN-ABOVE · BOLD/LARGE-TRACE STUDIES — ${VARIANTS.length} renders → ${OUT}/`);
|
||||
VARIANTS.forEach(renderVariant);
|
||||
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit3 · bold large-trace studies</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>Sun-above — bold, large traces on top · waves varied · sun faded back</h1>
|
||||
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name.replace(/^\d+_/, '')} — ${v.note}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||
212
tools/refined-backlit4.mjs
Normal file
@@ -0,0 +1,212 @@
|
||||
/* ============================================================
|
||||
refined-backlit4.mjs — ONE SUN SETTING, GOLDEN.
|
||||
The whole composition is built on φ (1.6180…):
|
||||
· CANVAS a golden rectangle, W = φ·H (landscape)
|
||||
· HORIZON the waterline splits the height 1/φ : 1 → y = W·0
|
||||
(sky = 0.382 H · sea = 0.618 H, ratio 1/φ)
|
||||
· DISK the sun's centre sits on the LEFT golden line
|
||||
(0.382 W) at the golden division of the sky; its
|
||||
radius is 0.382 of the sky band
|
||||
· TRACES the event vertex sits on the RIGHT golden line
|
||||
(0.618 W) where it meets the horizon
|
||||
· WAVES amplitude:gap = φ · near:far width = φ · the three
|
||||
stacked sheets stepped in opacity by 1/φ
|
||||
Each iteration re-rolls only the bubble-trace seed; the golden
|
||||
armature is identical. Grain + distressing stay on.
|
||||
|
||||
Usage: node tools/refined-backlit4.mjs [width]
|
||||
→ fable/REFINED_Backlit4/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { makeRng } from '../src/rng.js';
|
||||
|
||||
const PHI = (1 + Math.sqrt(5)) / 2; // 1.6180339887…
|
||||
const IP = 1 / PHI; // 0.6180… (long section)
|
||||
const IP2 = 1 / (PHI * PHI); // 0.3820… (short section)
|
||||
|
||||
const W = +(process.argv[2] || 1001);
|
||||
const H = Math.round(W * PHI); // golden rectangle, PORTRAIT (H = φ·W)
|
||||
const OUT = 'fable/REFINED_Backlit4';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = Math.min(W, H) / 1000; // stroke unit keyed to the short side (width)
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- golden armature (px) ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON_F = IP2; // waterline at 0.382 H → sky:sea = 1/φ
|
||||
const HY = HORIZON_F * H;
|
||||
const SKY_H = HY; // sky band height
|
||||
const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02); // renderSVG's isotropic scene→px scale
|
||||
const XL = IP2 * W; // left golden line 0.382 W
|
||||
const XR = IP * W; // right golden line 0.618 W
|
||||
const SUN = { x: XL, y: IP * SKY_H }; // sun centre: left line, 0.618 down the sky
|
||||
const SUN_R = IP2 * SKY_H; // sun radius = 0.382 of the sky band
|
||||
const VERTEX = { x: XR, y: HY }; // event vertex: right line ∩ horizon
|
||||
|
||||
/* ---- film / grain / aging, now golden-canvas sized ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * W, y0 = rng() * H * 0.4, len = (0.3 + rng() * 0.6) * H;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" filter="url(#fox)"/>
|
||||
<rect width="${W}" height="${H}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- the golden sea: three sheets, EQUAL line count, φ-ratioed waves ---- */
|
||||
function deck(mirror) {
|
||||
const base = {
|
||||
mode: 'plate', width: W, height: H, horizon: HORIZON_F,
|
||||
wFar: 0.5, wNear: 0.5 * PHI, // near:far width = φ
|
||||
overlap: PHI, // wave amplitude : row gap = φ
|
||||
rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
|
||||
chaos: 0.82, blips: 1.5,
|
||||
};
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(H, { ...base, salt: (mirror ? 'mfield' : 'field') + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(0.82 * 0.8, 0.82, t),
|
||||
strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- the event: golden-placed disk + traces ---- */
|
||||
function bcAssembleParams(seed, over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4,
|
||||
shockStriations: 1.0, diskHollow: 1.0,
|
||||
depth: 0.3, aging: 0.28,
|
||||
// event + shock both at scene origin; each is PLACED on the page by origin
|
||||
eventX: 0, eventY: 0, shockX: 0, shockY: 0,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2,
|
||||
shockSize: SUN_R / S_SCALE, // radius = 0.382 of the sky band
|
||||
shockIntensity: 0.8,
|
||||
size: 1.6, // larger bubbles
|
||||
canvasW: W, canvasH: H,
|
||||
}, over, { seed });
|
||||
return p;
|
||||
}
|
||||
|
||||
const TRACE_ZOOM = 2.1; // the evidence grows past the frame
|
||||
|
||||
function renderVariant(v, i) {
|
||||
const sun = v.mirror ? { x: XR, y: SUN.y } : SUN;
|
||||
const vtx = v.mirror ? { x: XL, y: HY } : VERTEX;
|
||||
|
||||
const p = bcAssembleParams(`${SEED}#${String(i).padStart(2, '0')}`);
|
||||
const scene = generateScene(p);
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
|
||||
const tracksImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: scene.tracks, shock: null },
|
||||
{ ...p, emit: ['bubble'], originX: vtx.x, originY: vtx.y, sceneZoom: TRACE_ZOOM }, W));
|
||||
const diskImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: [], shock: scene.shock },
|
||||
{ ...p, emit: ['disk'], originX: sun.x, originY: sun.y }, W));
|
||||
|
||||
const sheets = deck(v.mirror).map(dataUri);
|
||||
const film = dataUri(filmSVG({ seed: 41, density: 0.6 }));
|
||||
const grain = dataUri(grainSVG({ amount: 0.52 }));
|
||||
const aging = dataUri(agingSVG({ seed: 5 + i }));
|
||||
|
||||
const hpx = HY.toFixed(1);
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${W}" height="${H}" href="${href}" ${attrs}/>`;
|
||||
// three sea planes stepped in opacity by 1/φ (front → back)
|
||||
const OP = [0.95 * IP2, 0.95 * IP, 0.95];
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
||||
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="diskback" x="-12%" y="-12%" width="124%" height="124%"><feGaussianBlur stdDeviation="${(2.2 * u).toFixed(2)}"/></filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${W}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg, 'filter="url(#diskback)" opacity="0.5"')}</g>
|
||||
${IMG(sheets[0], `filter="url(#b3)" opacity="${OP[0].toFixed(2)}"`)}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)" opacity="0.85"')}</g>
|
||||
<rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="${OP[1].toFixed(2)}"`)}
|
||||
${IMG(sheets[2], `opacity="${OP[2].toFixed(2)}"`)}
|
||||
${IMG(tracksImg)}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, 'opacity="0.45" style="mix-blend-mode:multiply"')}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} sun(${sun.x.toFixed(0)},${sun.y.toFixed(0)}) vtx(${vtx.x.toFixed(0)},${vtx.y.toFixed(0)}) ${v.mirror ? 'mirrored' : ''}`);
|
||||
}
|
||||
|
||||
const VARIANTS = [
|
||||
{ name: '00_golden', mirror: false },
|
||||
{ name: '01_golden', mirror: false },
|
||||
{ name: '02_golden', mirror: false },
|
||||
{ name: '03_golden-mirror', mirror: true },
|
||||
{ name: '04_golden-mirror', mirror: true },
|
||||
{ name: '05_golden-mirror', mirror: true },
|
||||
];
|
||||
|
||||
console.log(`ONE SUN SETTING · GOLDEN — ${W}×${H} (φ) — ${VARIANTS.length} renders → ${OUT}/`);
|
||||
VARIANTS.forEach(renderVariant);
|
||||
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit4 · golden</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — golden (${W}×${H}, φ)</h1>
|
||||
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||
213
tools/refined-backlit5.mjs
Normal file
@@ -0,0 +1,213 @@
|
||||
/* ============================================================
|
||||
refined-backlit4.mjs — ONE SUN SETTING, GOLDEN.
|
||||
The whole composition is built on φ (1.6180…):
|
||||
· CANVAS a golden rectangle, W = φ·H (landscape)
|
||||
· HORIZON the waterline splits the height 1/φ : 1 → y = W·0
|
||||
(sky = 0.382 H · sea = 0.618 H, ratio 1/φ)
|
||||
· DISK the sun's centre sits on the LEFT golden line
|
||||
(0.382 W) at the golden division of the sky; its
|
||||
radius is 0.382 of the sky band
|
||||
· TRACES the event vertex sits on the RIGHT golden line
|
||||
(0.618 W) where it meets the horizon
|
||||
· WAVES amplitude:gap = φ · near:far width = φ · the three
|
||||
stacked sheets stepped in opacity by 1/φ
|
||||
Each iteration re-rolls only the bubble-trace seed; the golden
|
||||
armature is identical. Grain + distressing stay on.
|
||||
|
||||
Usage: node tools/refined-backlit4.mjs [width]
|
||||
→ fable/REFINED_Backlit5/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { makeRng } from '../src/rng.js';
|
||||
|
||||
const PHI = (1 + Math.sqrt(5)) / 2; // 1.6180339887…
|
||||
const IP = 1 / PHI; // 0.6180… (long section)
|
||||
const IP2 = 1 / (PHI * PHI); // 0.3820… (short section)
|
||||
|
||||
const W = +(process.argv[2] || 1001);
|
||||
const H = Math.round(W * PHI); // golden rectangle, PORTRAIT (H = φ·W)
|
||||
const OUT = 'fable/REFINED_Backlit5';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = Math.min(W, H) / 1000; // stroke unit keyed to the short side (width)
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- golden armature (px) ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON_F = IP2; // waterline at 0.382 H → sky:sea = 1/φ
|
||||
const HY = HORIZON_F * H;
|
||||
const SKY_H = HY; // sky band height
|
||||
const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02); // renderSVG's isotropic scene→px scale
|
||||
const XL = IP2 * W; // left golden line 0.382 W
|
||||
const XR = IP * W; // right golden line 0.618 W
|
||||
const SUN = { x: XL, y: IP * SKY_H }; // sun centre: left line, 0.618 down the sky
|
||||
const SUN_R = IP2 * SKY_H; // sun radius = 0.382 of the sky band
|
||||
const VERTEX = { x: XR, y: HY }; // event vertex: right line ∩ horizon
|
||||
|
||||
/* ---- film / grain / aging, now golden-canvas sized ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * W, y0 = rng() * H * 0.4, len = (0.3 + rng() * 0.6) * H;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" filter="url(#fox)"/>
|
||||
<rect width="${W}" height="${H}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- the golden sea: three sheets, EQUAL line count, φ-ratioed waves ---- */
|
||||
function deck(mirror) {
|
||||
const base = {
|
||||
mode: 'plate', width: W, height: H, horizon: HORIZON_F,
|
||||
wFar: 0.5, wNear: 0.5 * PHI, // near:far width = φ
|
||||
overlap: PHI, // wave amplitude : row gap = φ
|
||||
rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
|
||||
chaos: 0.82, blips: 1.5,
|
||||
};
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(H, { ...base, salt: (mirror ? 'mfield' : 'field') + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(0.82 * 0.8, 0.82, t),
|
||||
strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- the event: golden-placed disk + traces ---- */
|
||||
function bcAssembleParams(seed, over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4,
|
||||
shockStriations: 1.0, diskHollow: 1.0,
|
||||
depth: 0.3, aging: 0.28,
|
||||
// event + shock both at scene origin; each is PLACED on the page by origin
|
||||
eventX: 0, eventY: 0, shockX: 0, shockY: 0,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2,
|
||||
shockSize: SUN_R / S_SCALE, // radius = 0.382 of the sky band
|
||||
shockIntensity: 0.8,
|
||||
size: 1.6, // larger bubbles
|
||||
bubbleHollow: 1, bubbleWeight: 0.7, // delicate hollow bubbles: pale core, saturated rim
|
||||
canvasW: W, canvasH: H,
|
||||
}, over, { seed });
|
||||
return p;
|
||||
}
|
||||
|
||||
const TRACE_ZOOM = 2.1; // the evidence grows past the frame
|
||||
|
||||
function renderVariant(v, i) {
|
||||
const sun = v.mirror ? { x: XR, y: SUN.y } : SUN;
|
||||
const vtx = v.mirror ? { x: XL, y: HY } : VERTEX;
|
||||
|
||||
const p = bcAssembleParams(`${SEED}#${String(i).padStart(2, '0')}`);
|
||||
const scene = generateScene(p);
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
|
||||
const tracksImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: scene.tracks, shock: null },
|
||||
{ ...p, emit: ['bubble'], originX: vtx.x, originY: vtx.y, sceneZoom: TRACE_ZOOM }, W));
|
||||
const diskImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: [], shock: scene.shock },
|
||||
{ ...p, emit: ['disk'], originX: sun.x, originY: sun.y }, W));
|
||||
|
||||
const sheets = deck(v.mirror).map(dataUri);
|
||||
const film = dataUri(filmSVG({ seed: 41, density: 0.6 }));
|
||||
const grain = dataUri(grainSVG({ amount: 0.52 }));
|
||||
const aging = dataUri(agingSVG({ seed: 5 + i }));
|
||||
|
||||
const hpx = HY.toFixed(1);
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${W}" height="${H}" href="${href}" ${attrs}/>`;
|
||||
// three sea planes stepped in opacity by 1/φ (front → back)
|
||||
const OP = [0.95 * IP2, 0.95 * IP, 0.95];
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
||||
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="diskback" x="-12%" y="-12%" width="124%" height="124%"><feGaussianBlur stdDeviation="${(2.2 * u).toFixed(2)}"/></filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${W}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg, 'filter="url(#diskback)" opacity="0.5"')}</g>
|
||||
${IMG(sheets[0], `filter="url(#b3)" opacity="${OP[0].toFixed(2)}"`)}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)" opacity="0.85"')}</g>
|
||||
<rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="${OP[1].toFixed(2)}"`)}
|
||||
${IMG(sheets[2], `opacity="${OP[2].toFixed(2)}"`)}
|
||||
${IMG(tracksImg)}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, 'opacity="0.45" style="mix-blend-mode:multiply"')}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} sun(${sun.x.toFixed(0)},${sun.y.toFixed(0)}) vtx(${vtx.x.toFixed(0)},${vtx.y.toFixed(0)}) ${v.mirror ? 'mirrored' : ''}`);
|
||||
}
|
||||
|
||||
const VARIANTS = [
|
||||
{ name: '00_golden', mirror: false },
|
||||
{ name: '01_golden', mirror: false },
|
||||
{ name: '02_golden', mirror: false },
|
||||
{ name: '03_golden-mirror', mirror: true },
|
||||
{ name: '04_golden-mirror', mirror: true },
|
||||
{ name: '05_golden-mirror', mirror: true },
|
||||
];
|
||||
|
||||
console.log(`ONE SUN SETTING · GOLDEN · DELICATE BUBBLES — ${W}×${H} (φ) — ${VARIANTS.length} renders → ${OUT}/`);
|
||||
VARIANTS.forEach(renderVariant);
|
||||
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit5 · golden · delicate bubbles</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(2,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — golden (${W}×${H}, φ)</h1>
|
||||
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||
223
tools/refined-backlit6.mjs
Normal file
@@ -0,0 +1,223 @@
|
||||
/* ============================================================
|
||||
refined-backlit4.mjs — ONE SUN SETTING, GOLDEN.
|
||||
The whole composition is built on φ (1.6180…):
|
||||
· CANVAS a golden rectangle, W = φ·H (landscape)
|
||||
· HORIZON the waterline splits the height 1/φ : 1 → y = W·0
|
||||
(sky = 0.382 H · sea = 0.618 H, ratio 1/φ)
|
||||
· DISK the sun's centre sits on the LEFT golden line
|
||||
(0.382 W) at the golden division of the sky; its
|
||||
radius is 0.382 of the sky band
|
||||
· TRACES the event vertex sits on the RIGHT golden line
|
||||
(0.618 W) where it meets the horizon
|
||||
· WAVES amplitude:gap = φ · near:far width = φ · the three
|
||||
stacked sheets stepped in opacity by 1/φ
|
||||
Each iteration re-rolls only the bubble-trace seed; the golden
|
||||
armature is identical. Grain + distressing stay on.
|
||||
|
||||
Usage: node tools/refined-backlit4.mjs [width]
|
||||
→ fable/REFINED_Backlit6/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { makeRng } from '../src/rng.js';
|
||||
|
||||
const PHI = (1 + Math.sqrt(5)) / 2; // 1.6180339887…
|
||||
const IP = 1 / PHI; // 0.6180… (long section)
|
||||
const IP2 = 1 / (PHI * PHI); // 0.3820… (short section)
|
||||
|
||||
const W = +(process.argv[2] || 1001);
|
||||
const H = Math.round(W * PHI); // golden rectangle, PORTRAIT (H = φ·W)
|
||||
const OUT = 'fable/REFINED_Backlit6';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = Math.min(W, H) / 1000; // stroke unit keyed to the short side (width)
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- golden armature (px) ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON_F = IP2; // waterline at 0.382 H → sky:sea = 1/φ
|
||||
const HY = HORIZON_F * H;
|
||||
const SKY_H = HY; // sky band height
|
||||
const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02); // renderSVG's isotropic scene→px scale
|
||||
const XL = IP2 * W; // left golden line 0.382 W
|
||||
const XR = IP * W; // right golden line 0.618 W
|
||||
const SUN = { x: XL, y: IP * SKY_H }; // sun centre: left line, 0.618 down the sky
|
||||
const SUN_R = IP2 * SKY_H; // sun radius = 0.382 of the sky band
|
||||
const VERTEX = { x: XR, y: HY }; // event vertex: right line ∩ horizon
|
||||
|
||||
/* ---- film / grain / aging, now golden-canvas sized ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * W, y0 = rng() * H * 0.4, len = (0.3 + rng() * 0.6) * H;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" filter="url(#fox)"/>
|
||||
<rect width="${W}" height="${H}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- the golden sea: three sheets, EQUAL line count, φ-ratioed waves ---- */
|
||||
function deck(mirror) {
|
||||
const base = {
|
||||
mode: 'plate', width: W, height: H, horizon: HORIZON_F,
|
||||
wFar: 0.5, wNear: 0.5 * PHI, // near:far width = φ
|
||||
overlap: PHI, // wave amplitude : row gap = φ
|
||||
rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
|
||||
chaos: 0.82, blips: 1.5,
|
||||
};
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(H, { ...base, salt: (mirror ? 'mfield' : 'field') + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(0.82 * 0.8, 0.82, t),
|
||||
strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- the event: golden-placed disk + traces ---- */
|
||||
function bcAssembleParams(seed, over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4,
|
||||
shockStriations: 1.0, diskHollow: 1.0,
|
||||
depth: 0.3, aging: 0.28,
|
||||
// event + shock both at scene origin; each is PLACED on the page by origin
|
||||
eventX: 0, eventY: 0, shockX: 0, shockY: 0,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2,
|
||||
shockSize: SUN_R / S_SCALE, // radius = 0.382 of the sky band
|
||||
shockIntensity: 0.8,
|
||||
size: 1.6, // larger bubbles
|
||||
bubbleHollow: 1, bubbleWeight: 0.7, // delicate hollow bubbles: pale core, saturated rim
|
||||
bubbleSoft: 0.7, // softer, wider rings (was 0.3)
|
||||
bubbleOpacity: +(process.env.BUBOP ?? 0.9), // master bubble opacity slider — start at 90%
|
||||
canvasW: W, canvasH: H,
|
||||
}, over, { seed });
|
||||
return p;
|
||||
}
|
||||
|
||||
const TRACE_ZOOM = 2.1; // the evidence grows past the frame
|
||||
|
||||
function renderVariant(v, i) {
|
||||
const sun = v.mirror ? { x: XR, y: SUN.y } : SUN;
|
||||
const vtx = v.mirror ? { x: XL, y: HY } : VERTEX;
|
||||
|
||||
const p = bcAssembleParams(v.seed ?? `${SEED}#${String(i).padStart(2, '0')}`,
|
||||
v.bubOp != null ? { bubbleOpacity: v.bubOp } : {});
|
||||
const scene = generateScene(p);
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
|
||||
const tracksImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: scene.tracks, shock: null },
|
||||
{ ...p, emit: ['bubble'], originX: vtx.x, originY: vtx.y, sceneZoom: TRACE_ZOOM }, W));
|
||||
const diskImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: [], shock: scene.shock },
|
||||
{ ...p, emit: ['disk'], originX: sun.x, originY: sun.y }, W));
|
||||
|
||||
const sheets = deck(v.mirror).map(dataUri);
|
||||
const film = dataUri(filmSVG({ seed: 41, density: 0.6 }));
|
||||
const grain = dataUri(grainSVG({ amount: 0.52 }));
|
||||
const aging = dataUri(agingSVG({ seed: v.agingSeed ?? (5 + i) }));
|
||||
|
||||
const hpx = HY.toFixed(1);
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${W}" height="${H}" href="${href}" ${attrs}/>`;
|
||||
// three sea planes stepped in opacity by 1/φ (front → back)
|
||||
const OP = [0.95 * IP2, 0.95 * IP, 0.95];
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
||||
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="diskback" x="-12%" y="-12%" width="124%" height="124%"><feGaussianBlur stdDeviation="${(2.2 * u).toFixed(2)}"/></filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${W}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg, 'filter="url(#diskback)" opacity="0.5"')}</g>
|
||||
${IMG(sheets[0], `filter="url(#b3)" opacity="${OP[0].toFixed(2)}"`)}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)" opacity="0.85"')}</g>
|
||||
<rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="${OP[1].toFixed(2)}"`)}
|
||||
${IMG(sheets[2], `opacity="${OP[2].toFixed(2)}"`)}
|
||||
${IMG(tracksImg)}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, 'opacity="0.45" style="mix-blend-mode:multiply"')}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} bubbleOpacity=${(v.bubOp ?? 0.9).toFixed(2)} ${v.mirror ? 'mirrored' : ''}`);
|
||||
}
|
||||
|
||||
// OPACITY SWEEP — one composition (the liked mirror event, fixed seed), the
|
||||
// bubble-opacity slider stepped 0.50 → 0.95. Only the ink density changes.
|
||||
const SWEEP_SEED = `${SEED}#03`;
|
||||
const VARIANTS = [0.20, 0.32, 0.44, 0.56, 0.68, 0.80].map(op => ({
|
||||
name: `op-${Math.round(op * 100)}`, mirror: true, seed: SWEEP_SEED, bubOp: op, agingSeed: 8,
|
||||
}));
|
||||
|
||||
const _UNUSED = [
|
||||
{ name: '00_golden', mirror: false },
|
||||
{ name: '01_golden', mirror: false },
|
||||
{ name: '02_golden', mirror: false },
|
||||
{ name: '03_golden-mirror', mirror: true },
|
||||
{ name: '04_golden-mirror', mirror: true },
|
||||
{ name: '05_golden-mirror', mirror: true },
|
||||
];
|
||||
|
||||
console.log(`ONE SUN SETTING · GOLDEN · OPACITY SWEEP — ${W}×${H} (φ) — ${VARIANTS.length} renders → ${OUT}/`);
|
||||
VARIANTS.forEach(renderVariant);
|
||||
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit6 · bubble-opacity sweep</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — bubble-opacity sweep 20→80% (${W}×${H}, φ)</h1>
|
||||
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||
226
tools/refined-backlit7.mjs
Normal file
@@ -0,0 +1,226 @@
|
||||
/* ============================================================
|
||||
refined-backlit4.mjs — ONE SUN SETTING, GOLDEN.
|
||||
The whole composition is built on φ (1.6180…):
|
||||
· CANVAS a golden rectangle, W = φ·H (landscape)
|
||||
· HORIZON the waterline splits the height 1/φ : 1 → y = W·0
|
||||
(sky = 0.382 H · sea = 0.618 H, ratio 1/φ)
|
||||
· DISK the sun's centre sits on the LEFT golden line
|
||||
(0.382 W) at the golden division of the sky; its
|
||||
radius is 0.382 of the sky band
|
||||
· TRACES the event vertex sits on the RIGHT golden line
|
||||
(0.618 W) where it meets the horizon
|
||||
· WAVES amplitude:gap = φ · near:far width = φ · the three
|
||||
stacked sheets stepped in opacity by 1/φ
|
||||
Each iteration re-rolls only the bubble-trace seed; the golden
|
||||
armature is identical. Grain + distressing stay on.
|
||||
|
||||
Usage: node tools/refined-backlit4.mjs [width]
|
||||
→ fable/REFINED_Backlit7/
|
||||
============================================================ */
|
||||
import { writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { carpetSVG } from '../src/qft/carpet.js';
|
||||
import { generateScene } from '../src/scene/scene.js';
|
||||
import { renderSVG } from '../src/render/svgVector.js';
|
||||
import { paramsFromSeed as bcParams } from '../src/scene/params.js';
|
||||
import { GROUPS, TOGGLES, FIXED } from '../src/ui/controls.js';
|
||||
import { makeRng } from '../src/rng.js';
|
||||
|
||||
const PHI = (1 + Math.sqrt(5)) / 2; // 1.6180339887…
|
||||
const IP = 1 / PHI; // 0.6180… (long section)
|
||||
const IP2 = 1 / (PHI * PHI); // 0.3820… (short section)
|
||||
|
||||
const W = +(process.argv[2] || 1001);
|
||||
const H = Math.round(W * PHI); // golden rectangle, PORTRAIT (H = φ·W)
|
||||
const OUT = 'fable/REFINED_Backlit7';
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const u = Math.min(W, H) / 1000; // stroke unit keyed to the short side (width)
|
||||
const dataUri = (svg) => 'data:image/svg+xml;base64,' + Buffer.from(svg).toString('base64');
|
||||
|
||||
/* ---- golden armature (px) ---- */
|
||||
const SEED = 'MESON-5113';
|
||||
const BASE = 'rgb(227,220,200)';
|
||||
const HORIZON_F = IP2; // waterline at 0.382 H → sky:sea = 1/φ
|
||||
const HY = HORIZON_F * H;
|
||||
const SKY_H = HY; // sky band height
|
||||
const S_SCALE = (Math.min(W, H) / 2) * (1 - 0.02); // renderSVG's isotropic scene→px scale
|
||||
const XL = IP2 * W; // left golden line 0.382 W
|
||||
const XR = IP * W; // right golden line 0.618 W
|
||||
const SUN = { x: XL, y: IP * SKY_H }; // sun centre: left line, 0.618 down the sky
|
||||
const SUN_R = IP2 * SKY_H; // sun radius = 0.382 of the sky band
|
||||
const VERTEX = { x: XR, y: HY }; // event vertex: right line ∩ horizon
|
||||
|
||||
/* ---- film / grain / aging, now golden-canvas sized ---- */
|
||||
function filmSVG(o = {}) {
|
||||
const { seed = 7, freq = 0.0016, octaves = 4, tone = [236, 228, 208], density = 0.55 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="fog" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="${freq}" numOctaves="${octaves}" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${density} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#fog)"/></svg>`;
|
||||
}
|
||||
function grainSVG(o = {}) {
|
||||
const { seed = 19, tone = [38, 32, 26], amount = 0.5 } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs><filter id="g" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="${seed}" stitchTiles="stitch" result="n"/>
|
||||
<feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${amount} 0"/>
|
||||
</filter></defs><rect width="${W}" height="${H}" filter="url(#g)"/></svg>`;
|
||||
}
|
||||
function agingSVG(o = {}) {
|
||||
const { seed = 5, scratches = 8, dust = 0.6, foxing = 0.62, tone = [60, 48, 34] } = o;
|
||||
const t = tone.map(v => (v / 255).toFixed(3));
|
||||
const rng = makeRng(SEED, 'aging' + seed);
|
||||
let lines = '';
|
||||
for (let i = 0; i < scratches; i++) {
|
||||
const x = rng() * W, y0 = rng() * H * 0.4, len = (0.3 + rng() * 0.6) * H;
|
||||
const x2 = x + (rng() - 0.5) * 40, y2 = y0 + len;
|
||||
lines += `<line x1="${x.toFixed(0)}" y1="${y0.toFixed(0)}" x2="${x2.toFixed(0)}" y2="${y2.toFixed(0)}" stroke="rgb(${tone.join(',')})" stroke-opacity="${(0.05 + rng() * 0.12).toFixed(3)}" stroke-width="${(0.4 + rng() * 0.8).toFixed(2)}"/>`;
|
||||
}
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="dust" x="0" y="0" width="100%" height="100%"><feTurbulence type="turbulence" baseFrequency="0.5" numOctaves="2" seed="${seed + 5}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 ${t[0]} 0 0 0 0 ${t[1]} 0 0 0 0 ${t[2]} 0 0 0 ${(dust * 0.5).toFixed(3)} -0.18"/></filter>
|
||||
<filter id="fox" x="0" y="0" width="100%" height="100%"><feTurbulence type="fractalNoise" baseFrequency="0.004" numOctaves="3" seed="${seed + 9}" stitchTiles="stitch" result="n"/><feColorMatrix in="n" type="matrix" values="0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.16 0 0 0 ${(foxing * 0.32).toFixed(3)} -0.12"/></filter>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" filter="url(#fox)"/>
|
||||
<rect width="${W}" height="${H}" filter="url(#dust)"/>
|
||||
${lines}</svg>`;
|
||||
}
|
||||
|
||||
/* ---- the golden sea: three sheets, EQUAL line count, φ-ratioed waves ---- */
|
||||
function deck(mirror) {
|
||||
const base = {
|
||||
mode: 'plate', width: W, height: H, horizon: HORIZON_F,
|
||||
wFar: 0.5, wNear: 0.5 * PHI, // near:far width = φ
|
||||
overlap: PHI, // wave amplitude : row gap = φ
|
||||
rows: 48, mound: 0.4, sat: 0.58, lightNear: 0.33, lightFar: 0.56,
|
||||
chaos: 0.82, blips: 1.5,
|
||||
};
|
||||
const lerp = (a, b, t) => a + (b - a) * t;
|
||||
const strokes = [{ near: 2.6, far: 1.0 }, { near: 1.6, far: 0.6 }, { near: 1.0, far: 0.38 }];
|
||||
return [0, 1, 2].map(i => {
|
||||
const t = i / 2;
|
||||
return carpetSVG(H, { ...base, salt: (mirror ? 'mfield' : 'field') + i,
|
||||
hue: lerp(0.55, 0.47, t), hue2: lerp(0.55, 0.47, t) + 0.035,
|
||||
chaos: lerp(0.82 * 0.8, 0.82, t),
|
||||
strokeNear: strokes[i].near * 1.35, strokeFar: strokes[i].far * 1.35 });
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- the event: golden-placed disk + traces ---- */
|
||||
function bcAssembleParams(seed, over = {}) {
|
||||
const p = { ...FIXED, ...bcParams(SEED) };
|
||||
for (const g of GROUPS) for (const c of g.controls) if (!(c.id in p)) p[c.id] = c.value;
|
||||
for (const t of TOGGLES) if (!(t.id in p)) p[t.id] = t.value;
|
||||
Object.assign(p, {
|
||||
invert: true, transparentPaper: true, vign: 0,
|
||||
showHeader: false, showBoundary: false, showFiducials: false,
|
||||
annotate: 0, reseau: 0, filmEdge: false, splice: false, artifacts: 0,
|
||||
palette: 'magbeta', saturation: 1.0,
|
||||
diskBubbles: false, diskPressure: 0.9, diskSoften: 1.4,
|
||||
shockStriations: 1.0, diskHollow: 1.0,
|
||||
depth: 0.3, aging: 0.28,
|
||||
// event + shock both at scene origin; each is PLACED on the page by origin
|
||||
eventX: 0, eventY: 0, shockX: 0, shockY: 0,
|
||||
sweepers: 5, primaries: 17, eloss: 0.34, deltaRate: 0.8,
|
||||
bgEvents: 2,
|
||||
shockSize: SUN_R / S_SCALE, // radius = 0.382 of the sky band
|
||||
shockIntensity: 0.8,
|
||||
size: 1.6, // larger bubbles
|
||||
bubbleHollow: 1, bubbleWeight: 0.7, // delicate hollow bubbles: pale core, saturated rim
|
||||
bubbleSoft: 0.7, // softer, wider rings (was 0.3)
|
||||
bubbleMerge: 1, // crowded bubbles fuse to flat union (uniform lace, backlit-even)
|
||||
bubbleOpacity: +(process.env.BUBOP ?? 0.9), // = uniform target density / backlit brightness
|
||||
canvasW: W, canvasH: H,
|
||||
}, over, { seed });
|
||||
return p;
|
||||
}
|
||||
|
||||
const TRACE_ZOOM = 2.1; // the evidence grows past the frame
|
||||
|
||||
function renderVariant(v, i) {
|
||||
const sun = v.mirror ? { x: XR, y: SUN.y } : SUN;
|
||||
const vtx = v.mirror ? { x: XL, y: HY } : VERTEX;
|
||||
|
||||
const p = bcAssembleParams(v.seed ?? `${SEED}#${String(i).padStart(2, '0')}`,
|
||||
v.bubOp != null ? { bubbleOpacity: v.bubOp } : {});
|
||||
const scene = generateScene(p);
|
||||
const bare = { ...scene, instrument: null, artifacts: null, media: null };
|
||||
|
||||
const tracksImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: scene.tracks, shock: null },
|
||||
{ ...p, emit: ['bubble'], originX: vtx.x, originY: vtx.y, sceneZoom: TRACE_ZOOM }, W));
|
||||
const diskImg = dataUri(renderSVG(
|
||||
{ ...bare, tracks: [], shock: scene.shock },
|
||||
{ ...p, emit: ['disk'], originX: sun.x, originY: sun.y }, W));
|
||||
|
||||
const sheets = deck(v.mirror).map(dataUri);
|
||||
const film = dataUri(filmSVG({ seed: 41, density: 0.6 }));
|
||||
const grain = dataUri(grainSVG({ amount: 0.52 }));
|
||||
const aging = dataUri(agingSVG({ seed: v.agingSeed ?? (5 + i) }));
|
||||
|
||||
const hpx = HY.toFixed(1);
|
||||
const IMG = (href, attrs = '') => `<image x="0" y="0" width="${W}" height="${H}" href="${href}" ${attrs}/>`;
|
||||
// three sea planes stepped in opacity by 1/φ (front → back)
|
||||
const OP = [0.95 * IP2, 0.95 * IP, 0.95];
|
||||
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||
<defs>
|
||||
<filter id="b3" x="-8%" y="-8%" width="116%" height="116%"><feGaussianBlur stdDeviation="${(2.6 * u).toFixed(2)}"/></filter>
|
||||
<filter id="b2" x="-6%" y="-6%" width="112%" height="112%"><feGaussianBlur stdDeviation="${(1.1 * u).toFixed(2)}"/></filter>
|
||||
<filter id="subD" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feColorMatrix type="matrix" values="0.80 0 0 0 0.012 0 0.93 0 0 0.024 0 0 0.95 0 0.024 0 0 0 1.00 0"/>
|
||||
<feGaussianBlur stdDeviation="${(0.6 * u).toFixed(2)}"/>
|
||||
</filter>
|
||||
<filter id="diskback" x="-12%" y="-12%" width="124%" height="124%"><feGaussianBlur stdDeviation="${(2.2 * u).toFixed(2)}"/></filter>
|
||||
<clipPath id="skyc"><rect x="0" y="0" width="${W}" height="${hpx}"/></clipPath>
|
||||
<clipPath id="seac"><rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}"/></clipPath>
|
||||
</defs>
|
||||
<rect width="${W}" height="${H}" fill="${BASE}"/>
|
||||
${IMG(film, 'opacity="0.6"')}
|
||||
<g clip-path="url(#skyc)">${IMG(diskImg, 'filter="url(#diskback)" opacity="0.5"')}</g>
|
||||
${IMG(sheets[0], `filter="url(#b3)" opacity="${OP[0].toFixed(2)}"`)}
|
||||
<g clip-path="url(#seac)">${IMG(diskImg, 'filter="url(#subD)" opacity="0.85"')}</g>
|
||||
<rect x="0" y="${hpx}" width="${W}" height="${(H - HY).toFixed(1)}" fill="rgb(96,138,128)" opacity="0.07"/>
|
||||
${IMG(sheets[1], `filter="url(#b2)" opacity="${OP[1].toFixed(2)}"`)}
|
||||
${IMG(sheets[2], `opacity="${OP[2].toFixed(2)}"`)}
|
||||
${IMG(tracksImg)}
|
||||
${IMG(aging, 'opacity="0.5" style="mix-blend-mode:multiply"')}
|
||||
${IMG(grain, 'opacity="0.45" style="mix-blend-mode:multiply"')}
|
||||
</svg>`;
|
||||
writeFileSync(`${OUT}/${v.name}.svg`, svg);
|
||||
console.log(` ${v.name} bubbleOpacity=${(v.bubOp ?? 0.9).toFixed(2)} ${v.mirror ? 'mirrored' : ''}`);
|
||||
}
|
||||
|
||||
// MERGE · UNIFORM LACE — one composition (the liked mirror event, fixed seed), the
|
||||
// bubble-opacity slider stepped 0.50 → 0.95. Only the ink density changes.
|
||||
const SWEEP_SEED = `${SEED}#03`;
|
||||
// MERGE — uniform-lace target sweep. bubOp is now the CAPPED union density
|
||||
// (dense knot = same density as a lone bubble), i.e. the backlit brightness.
|
||||
const VARIANTS = [0.35, 0.45, 0.55, 0.65, 0.75, 0.85].map(op => ({
|
||||
name: `merge-${Math.round(op * 100)}`, mirror: true, seed: SWEEP_SEED, bubOp: op, agingSeed: 8,
|
||||
}));
|
||||
|
||||
const _UNUSED = [
|
||||
{ name: '00_golden', mirror: false },
|
||||
{ name: '01_golden', mirror: false },
|
||||
{ name: '02_golden', mirror: false },
|
||||
{ name: '03_golden-mirror', mirror: true },
|
||||
{ name: '04_golden-mirror', mirror: true },
|
||||
{ name: '05_golden-mirror', mirror: true },
|
||||
];
|
||||
|
||||
console.log(`ONE SUN SETTING · GOLDEN · MERGE · UNIFORM LACE — ${W}×${H} (φ) — ${VARIANTS.length} renders → ${OUT}/`);
|
||||
VARIANTS.forEach(renderVariant);
|
||||
|
||||
writeFileSync(`${OUT}/index.html`, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>REFINED_Backlit6 · bubble-opacity sweep</title>
|
||||
<style>body{margin:0;background:#0b0b0b;color:#bbb;font:12px/1.5 ui-monospace,monospace;padding:26px}
|
||||
h1{font-weight:400;letter-spacing:.22em;text-transform:uppercase;font-size:13px;color:#d9b48a}
|
||||
.row{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:16px}
|
||||
figure{margin:0;background:#fff;overflow:hidden}img{width:100%;display:block}
|
||||
figcaption{padding:6px 8px;color:#e8e4d8;background:#111;font-size:11px}</style></head><body>
|
||||
<h1>One sun setting — merge · uniform-lace target sweep (${W}×${H}, φ)</h1>
|
||||
<div class="row">${VARIANTS.map(v => `<figure><img src="${v.name}.svg"><figcaption>${v.name}</figcaption></figure>`).join('')}</div>
|
||||
</body></html>`);
|
||||
console.log(`gallery -> ${OUT}/index.html`);
|
||||