@codincod/ornament 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +62 -2
- package/package.json +35 -3
- package/src/biome.ts +157 -0
- package/src/canvas/flat.ts +336 -0
- package/src/caustics.ts +81 -0
- package/src/census.ts +262 -0
- package/src/cephalopods.ts +1041 -0
- package/src/cloud.ts +308 -0
- package/src/crags.ts +974 -0
- package/src/drift.ts +435 -0
- package/src/eels.ts +667 -0
- package/src/fish_shape.ts +410 -0
- package/src/flora.ts +1955 -0
- package/src/flow_field.ts +491 -0
- package/src/jellies.ts +1794 -0
- package/src/nemos.ts +606 -0
- package/src/paint/bed.ts +231 -0
- package/src/paint/colour.ts +86 -0
- package/src/paint/frame.ts +204 -0
- package/src/paint/ink.ts +134 -0
- package/src/paint/life.ts +611 -0
- package/src/paint/light.ts +132 -0
- package/src/paint/pen.ts +225 -0
- package/src/paint/rock.ts +325 -0
- package/src/paint/sea.ts +809 -0
- package/src/paint/shade.ts +109 -0
- package/src/paint/sky.ts +377 -0
- package/src/paint/specimen.ts +300 -0
- package/src/paint/svg.ts +326 -0
- package/src/paint/trace.ts +164 -0
- package/src/passers.ts +730 -0
- package/src/perlin.ts +168 -0
- package/src/plenty.ts +65 -0
- package/src/puffers.ts +510 -0
- package/src/rays.ts +225 -0
- package/src/reef.ts +945 -0
- package/src/relics.ts +451 -0
- package/src/seabed.ts +525 -0
- package/src/shoal.ts +1658 -0
- package/src/sizes.ts +86 -0
- package/src/sun.ts +500 -0
- package/src/swarm.ts +587 -0
- package/src/visitors.ts +1936 -0
- package/src/walkers.ts +861 -0
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# codin-cod-ornament
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The sea behind [CodinCod](https://codincod.com): what lives in it, the weather
|
|
4
|
+
over it, and how each thing is drawn. Published as `@codincod/ornament`.
|
|
5
|
+
|
|
6
|
+
Outside `src/canvas/`, nothing here touches a browser. The modules decide where
|
|
7
|
+
a fish goes, what the sky is doing at this hour, and what each drawing looks
|
|
8
|
+
like, and hand that to a renderer. Three read it:
|
|
9
|
+
|
|
10
|
+
- CodinCod's site, whose `assets/js/ornament/` holds the SVG and WebGPU halves
|
|
11
|
+
and the LiveView hooks that start them, and draws with `src/canvas/` where
|
|
12
|
+
there is no graphics card.
|
|
13
|
+
- [codin-cod-maintenance](https://codeberg.org/reeven/codin-cod-maintenance),
|
|
14
|
+
the page codincod.com shows while it is down, which draws with `src/canvas/`
|
|
15
|
+
and bakes a still with `src/paint/svg.ts`.
|
|
16
|
+
- [seascape](https://github.com/reeveng/seascape), the desktop background, in a
|
|
17
|
+
bare V8 and in Qt's JavaScript engine.
|
|
18
|
+
|
|
19
|
+
`tsconfig.json` leaves the DOM out of `lib`, so `pnpm check` fails on any
|
|
20
|
+
module that reaches for one. `src/canvas/` is checked on its own, by
|
|
21
|
+
`tsconfig.canvas.json`, with the DOM.
|
|
22
|
+
|
|
23
|
+
## Layout
|
|
24
|
+
|
|
25
|
+
- `src/*.ts`: one simulation per kind of thing in the water or the sky, each
|
|
26
|
+
with its test beside it.
|
|
27
|
+
- `src/paint/`: what one drawing looks like, and `sea.ts`, which builds a whole
|
|
28
|
+
frame out of them. `ink.ts` is the colour arithmetic every renderer shares,
|
|
29
|
+
`colour.ts` reads a theme's colours back out of a stylesheet, and `svg.ts`
|
|
30
|
+
writes a frame as SVG markup, for a page that has to look right with no
|
|
31
|
+
script.
|
|
32
|
+
- `src/sizes.ts` and `src/census.ts`: how big things are and how many of them
|
|
33
|
+
there are.
|
|
34
|
+
- `src/canvas/flat.ts`: a frame onto a 2D canvas, the one module here that
|
|
35
|
+
needs the DOM.
|
|
36
|
+
|
|
37
|
+
The package ships its TypeScript source. Import a module by its path:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createShoal } from "@codincod/ornament/shoal.ts"
|
|
41
|
+
import { createSea } from "@codincod/ornament/paint/sea.ts"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm install
|
|
48
|
+
pnpm test # node --test over src/**/*.test.ts
|
|
49
|
+
pnpm check # tsc, with no DOM
|
|
50
|
+
pnpm fmt # biome
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Releasing
|
|
54
|
+
|
|
55
|
+
1. Bump `version`, then `pnpm publish`.
|
|
56
|
+
2. In CodinCod, raise the pin in `assets/package.json`. A caret range on a
|
|
57
|
+
`0.x` version stops at the next minor, so `^0.1.0` never picks up 0.2.0.
|
|
58
|
+
3. Run `pnpm install` from CodinCod's root, then `mix assets.build`.
|
|
59
|
+
4. In seascape, rebuild the bundle (`js/build.sh`, then `cargo build --release`).
|
|
60
|
+
|
|
61
|
+
## Licence
|
|
62
|
+
|
|
63
|
+
[AGPL-3.0](LICENSE), the same as CodinCod.
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codincod/ornament",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The sea behind CodinCod: what lives in it, the weather over it, how each thing is drawn, and a canvas to draw it on",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"codincod",
|
|
7
|
+
"generative",
|
|
8
|
+
"ornament",
|
|
9
|
+
"simulation"
|
|
10
|
+
],
|
|
11
|
+
"author": "CodinCod <hello@codincod.com>",
|
|
12
|
+
"license": "AGPL-3.0-only",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
"./*": "./src/*"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"!src/**/*.test.ts"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@biomejs/biome": "^2.5.15",
|
|
24
|
+
"@types/node": "^26.6.5",
|
|
25
|
+
"typescript": "^7.0.2"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://codincod.com/",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://codeberg.org/reeven/codin-cod-ornament"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.canvas.json",
|
|
34
|
+
"fmt": "biome check --write .",
|
|
35
|
+
"fmt:check": "biome check .",
|
|
36
|
+
"test": "node --test \"src/**/*.test.ts\""
|
|
37
|
+
}
|
|
6
38
|
}
|
package/src/biome.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The water everything is in, and the register of what is in it.
|
|
3
|
+
*
|
|
4
|
+
* Not a conductor. There is no score, nothing here decides what any animal
|
|
5
|
+
* does, and no call in this file makes anything happen. What it holds is a fact
|
|
6
|
+
* every animal in a sea has access to and none of them is told: what else is
|
|
7
|
+
* about, where, and how alarming it looks from a distance.
|
|
8
|
+
*
|
|
9
|
+
* ## Why the water needs a register at all
|
|
10
|
+
*
|
|
11
|
+
* Because the alternative is a director, and a director is what a sea does not
|
|
12
|
+
* have. A shark crossing a reef does not notify the clownfish. It swims, and
|
|
13
|
+
* the reef empties because every fish on it was watching the water and drew its
|
|
14
|
+
* own conclusion, at its own moment, at its own distance. That is why nothing
|
|
15
|
+
* in this ornament exposes a call anybody could fire at it. An animal is handed
|
|
16
|
+
* `about`, looks, and decides.
|
|
17
|
+
*
|
|
18
|
+
* The register is what makes that possible without every animal knowing about
|
|
19
|
+
* every other one. `nemos.ts`, `flora.ts` and `walkers.ts` each grew their own
|
|
20
|
+
* copy of one small shape while waiting for this, and all three said the same
|
|
21
|
+
* thing in their comments: a crab should not be importing a type from a
|
|
22
|
+
* clownfish. `Presence` is that shape, declared once, and a source of them is
|
|
23
|
+
* anything at all.
|
|
24
|
+
*
|
|
25
|
+
* ## A shark at the glass is not near a fish in the murk
|
|
26
|
+
*
|
|
27
|
+
* This water has depth and it is the dimension that gets forgotten,
|
|
28
|
+
* because a screen has only two. Everything in this ornament carries a `depth`:
|
|
29
|
+
* 0 is as far off as the water goes and 1 is up against the glass, and that
|
|
30
|
+
* range is most of the room in the scene. Two things can share a point on the
|
|
31
|
+
* screen and be a long way from each other.
|
|
32
|
+
*
|
|
33
|
+
* So a shark cruising past the camera has no business emptying a reef sitting
|
|
34
|
+
* back in the murk, however neatly the two line up. `abreast` is the share of
|
|
35
|
+
* that, and every reading in this ornament is weighed by it.
|
|
36
|
+
*
|
|
37
|
+
* ## Size and menace are different questions
|
|
38
|
+
*
|
|
39
|
+
* `size` decides how far off something is noticed, because that is what being
|
|
40
|
+
* seen underwater actually depends on. `menace` decides how much it matters
|
|
41
|
+
* once it has been. A turtle and a shark of the same length are the same
|
|
42
|
+
* silhouette at the same distance and they are not the same event, and a sea
|
|
43
|
+
* where they were is a sea where nothing that happens in it means anything.
|
|
44
|
+
*
|
|
45
|
+
* What either number turns into is not settled here. A clownfish quickens at
|
|
46
|
+
* something big and goes home for something hunting; a crab freezes at both; a
|
|
47
|
+
* crown of tentacles minds only what is nearly on top of it; a starfish never
|
|
48
|
+
* reacts to anything, because its answer takes hours and nobody is watching
|
|
49
|
+
* that long. One reading, many answers, and every one of them the animal's own.
|
|
50
|
+
*
|
|
51
|
+
* ## Nothing has to enter
|
|
52
|
+
*
|
|
53
|
+
* A water nobody entered is open water, and everything reading it goes about
|
|
54
|
+
* its day. That is not a degenerate case: it is what most of these scenes are,
|
|
55
|
+
* and it is why every `about` in this ornament is optional.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Something in the water, as much of it as another animal can make out.
|
|
60
|
+
*
|
|
61
|
+
* Where it is, how big it looks, and whether it looks like it hunts. Not what
|
|
62
|
+
* it is. Nothing reading this register can tell a shark from a turtle except by
|
|
63
|
+
* these numbers, which is the right amount for an animal to know: a small fish
|
|
64
|
+
* on a reef has never been told what species anything is either.
|
|
65
|
+
*/
|
|
66
|
+
export interface Presence {
|
|
67
|
+
/** 0 is as far off as this water goes, 1 is up against the glass. */
|
|
68
|
+
readonly depth: number
|
|
69
|
+
/**
|
|
70
|
+
* How much this looks like something that hunts, 0 through 1.
|
|
71
|
+
*
|
|
72
|
+
* 0 is a shape that has never worried anybody, which is most of what crosses
|
|
73
|
+
* a sea. 1 empties a reef. Left out it is 0, so anything entering the water
|
|
74
|
+
* without an opinion about itself is merely large.
|
|
75
|
+
*/
|
|
76
|
+
readonly menace?: number
|
|
77
|
+
/** How big it is across, in px, which is how far off it is noticed from. */
|
|
78
|
+
readonly size: number
|
|
79
|
+
readonly x: number
|
|
80
|
+
readonly y: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Something that says where it is when asked.
|
|
85
|
+
*
|
|
86
|
+
* A function rather than a list, because everything in a sea is moving and a
|
|
87
|
+
* list handed over once is a list of where things were. It returns however many
|
|
88
|
+
* it speaks for: one animal, a pod of them, or none at all this moment.
|
|
89
|
+
*/
|
|
90
|
+
export type Source = () => readonly Presence[]
|
|
91
|
+
|
|
92
|
+
export interface Biome {
|
|
93
|
+
/**
|
|
94
|
+
* Everything in the water at this moment.
|
|
95
|
+
*
|
|
96
|
+
* Gathered afresh on every ask, since nothing here keeps a clock and the
|
|
97
|
+
* answer is only true for as long as nothing has moved. The list belongs to
|
|
98
|
+
* the water and is refilled on the next ask, so read it and let it go rather
|
|
99
|
+
* than keeping it.
|
|
100
|
+
*/
|
|
101
|
+
about(): readonly Presence[]
|
|
102
|
+
/**
|
|
103
|
+
* Put something in the water. What comes back takes it out again.
|
|
104
|
+
*
|
|
105
|
+
* Whatever built the animal usually does this, and an animal that does it for
|
|
106
|
+
* itself is better still, since what a thing looks like to something small is
|
|
107
|
+
* a fact about that thing. Nothing checks. A sea is not a guest list.
|
|
108
|
+
*/
|
|
109
|
+
enter(source: Source): () => void
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* How far apart in the water two distances are before they stop mattering.
|
|
114
|
+
*
|
|
115
|
+
* A third of the way through, which is generous and meant to be. The depth of
|
|
116
|
+
* one of these scenes is not measured in anything, so this cannot be a real
|
|
117
|
+
* distance; what it has to do is stop the far murk and the near glass being the
|
|
118
|
+
* same place while leaving anything swimming at roughly your own distance
|
|
119
|
+
* plainly there. Wider than this and depth does nothing. Narrower and animals
|
|
120
|
+
* pass through each other unnoticed, which is worse than the fault it fixes.
|
|
121
|
+
*/
|
|
122
|
+
const REACH = 0.36
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* How nearly abreast two things are, 1 side by side and 0 a water apart.
|
|
126
|
+
*
|
|
127
|
+
* The one piece of arithmetic in this file, and it is geometry rather than
|
|
128
|
+
* judgement: how far apart two things are is the water's own business, and what
|
|
129
|
+
* either of them does about it is not. It falls off rather than cutting off,
|
|
130
|
+
* because a threshold makes a shark switch from harmless to terrifying between
|
|
131
|
+
* two frames of swimming gently forward.
|
|
132
|
+
*/
|
|
133
|
+
export function abreast(depth: number, other: number): number {
|
|
134
|
+
return Math.max(0, 1 - Math.abs(depth - other) / REACH)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function createBiome(): Biome {
|
|
138
|
+
const sources = new Set<Source>()
|
|
139
|
+
const gathered: Presence[] = []
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
about() {
|
|
143
|
+
gathered.length = 0
|
|
144
|
+
for (const source of sources) {
|
|
145
|
+
for (const one of source()) gathered.push(one)
|
|
146
|
+
}
|
|
147
|
+
return gathered
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
enter(source) {
|
|
151
|
+
sources.add(source)
|
|
152
|
+
return () => {
|
|
153
|
+
sources.delete(source)
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sea, onto a canvas, with nothing but the browser's own rasteriser.
|
|
3
|
+
*
|
|
4
|
+
* The renderer every machine has. `card.ts` is faster wherever there is a card
|
|
5
|
+
* to talk to, and this is what draws the water when there is not: an old
|
|
6
|
+
* browser, a machine without WebGPU, a device that answered the adapter request
|
|
7
|
+
* with nothing. The picture is the same picture.
|
|
8
|
+
*
|
|
9
|
+
* ## Two things a card does per pixel that a canvas cannot
|
|
10
|
+
*
|
|
11
|
+
* A fragment shader reads the water's own gradient at the height of every pixel
|
|
12
|
+
* it fills, and works out how far a light has given out at that same height. A
|
|
13
|
+
* canvas fill takes one colour, or one gradient built before the fill starts.
|
|
14
|
+
*
|
|
15
|
+
* So the height a colour is read at is decided per drawing here. Most of what
|
|
16
|
+
* swims is small enough that one reading is the truth: a fish is a few dozen px
|
|
17
|
+
* tall and the water does not change over that. What is not small says so, by
|
|
18
|
+
* fading down the box or by covering a good share of it, and those are drawn
|
|
19
|
+
* through a gradient with the same arithmetic sampled along it. Both renderers
|
|
20
|
+
* agree on the numbers; they disagree on how often they ask.
|
|
21
|
+
*
|
|
22
|
+
* ## Why the fill rule matters more than it looks
|
|
23
|
+
*
|
|
24
|
+
* An animal is several closed shapes meant to overlap: a body, a tail swung off
|
|
25
|
+
* its joint, a fin rooted inside the back. Filled as one path with the non-zero
|
|
26
|
+
* rule they are one silhouette, which is the animal. Filled one after another
|
|
27
|
+
* they are three shapes painted over each other, and at the weight ornament is
|
|
28
|
+
* drawn at every overlap shows as a bright seam. A canvas fills non-zero by
|
|
29
|
+
* default, so this gets it for nothing; the card has to be told.
|
|
30
|
+
*/
|
|
31
|
+
import { FORM, held, OWN, Reader, type View } from "../paint/frame.ts"
|
|
32
|
+
import { css, hueOf, leftAt, type Palette } from "../paint/ink.ts"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* How tall a drawing has to be, as a share of the box, before it is worth a
|
|
36
|
+
* gradient rather than one reading of the water.
|
|
37
|
+
*
|
|
38
|
+
* Under this the error is smaller than the step between two neighbouring values
|
|
39
|
+
* of an eight bit channel, so a gradient would cost a fill and change nothing.
|
|
40
|
+
*/
|
|
41
|
+
const TALL = 0.08
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* How many places a gradient is sampled at.
|
|
45
|
+
*
|
|
46
|
+
* Both the water's colour and how far a light has given out are curves, and a
|
|
47
|
+
* gradient between two stops is a straight line. This is where the two stop
|
|
48
|
+
* being told apart on a screen.
|
|
49
|
+
*/
|
|
50
|
+
const STOPS = 7
|
|
51
|
+
|
|
52
|
+
export interface Box {
|
|
53
|
+
height: number
|
|
54
|
+
width: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FlatOptions {
|
|
58
|
+
/**
|
|
59
|
+
* How far past the box a drawing may stand before it is skipped, as a share
|
|
60
|
+
* of the box.
|
|
61
|
+
*
|
|
62
|
+
* Nothing in the water knows where the edge of the picture is, so a fish that
|
|
63
|
+
* has left is still swum and a plant behind a mass of near rock is still cut.
|
|
64
|
+
* A renderer is the last chance to not pay for them.
|
|
65
|
+
*/
|
|
66
|
+
bleed?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What of the water the box is showing. */
|
|
70
|
+
interface Seen {
|
|
71
|
+
foot: number
|
|
72
|
+
left: number
|
|
73
|
+
right: number
|
|
74
|
+
top: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface Flat {
|
|
78
|
+
draw(floats: Float32Array, palette: Palette, box: Box, density?: number, view?: View | null): void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A canvas that will draw a frame of this water whenever it is handed one.
|
|
83
|
+
*
|
|
84
|
+
* It keeps no clock and owns no simulation. Something else steps the sea and
|
|
85
|
+
* hands over the block; all this does is put it on the screen.
|
|
86
|
+
*/
|
|
87
|
+
export function createFlat(canvas: HTMLCanvasElement, options: FlatOptions = {}): Flat | null {
|
|
88
|
+
const context = canvas.getContext("2d")
|
|
89
|
+
if (!context) return null
|
|
90
|
+
|
|
91
|
+
const bleed = options.bleed ?? 0.25
|
|
92
|
+
|
|
93
|
+
// Where each drawing is and how far back it stands, so the solid ones can be
|
|
94
|
+
// put in order. Kept between frames: a busy sea is a few thousand drawings,
|
|
95
|
+
// and three arrays of them rebuilt sixty times a second is the sort of
|
|
96
|
+
// housekeeping that costs more than the work it is for.
|
|
97
|
+
let places = new Int32Array(4096)
|
|
98
|
+
let lanes = new Float32Array(4096)
|
|
99
|
+
let order: number[] = []
|
|
100
|
+
|
|
101
|
+
const room = (needed: number) => {
|
|
102
|
+
if (needed > places.length) {
|
|
103
|
+
const size = 1 << Math.ceil(Math.log2(Math.max(16, needed)))
|
|
104
|
+
places = new Int32Array(size)
|
|
105
|
+
lanes = new Float32Array(size)
|
|
106
|
+
}
|
|
107
|
+
while (order.length < needed) order.push(order.length)
|
|
108
|
+
if (order.length > needed) order.length = needed
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
/**
|
|
113
|
+
* One frame, onto the canvas.
|
|
114
|
+
*
|
|
115
|
+
* `box` is the water's own size in the units it was laid out in, which is
|
|
116
|
+
* not the canvas's size in pixels: a screen at twice the density draws the
|
|
117
|
+
* same sea out of four times as many of them.
|
|
118
|
+
*/
|
|
119
|
+
draw(
|
|
120
|
+
floats: Float32Array,
|
|
121
|
+
palette: Palette,
|
|
122
|
+
box: Box,
|
|
123
|
+
density = 1,
|
|
124
|
+
view: View | null = null,
|
|
125
|
+
): void {
|
|
126
|
+
const camera = held(floats)
|
|
127
|
+
const reader = new Reader(floats)
|
|
128
|
+
|
|
129
|
+
context.setTransform(density, 0, 0, density, 0, 0)
|
|
130
|
+
context.clearRect(0, 0, box.width, box.height)
|
|
131
|
+
|
|
132
|
+
const middle = { x: box.width / 2, y: box.height / 2 }
|
|
133
|
+
|
|
134
|
+
if (view) {
|
|
135
|
+
context.translate(middle.x, middle.y)
|
|
136
|
+
context.scale(view.scale, view.scale)
|
|
137
|
+
context.translate(-view.x, -view.y)
|
|
138
|
+
} else {
|
|
139
|
+
// The frame, held by somebody rather than bolted to a tripod. It is
|
|
140
|
+
// drawn a little larger than its box to pay for the wander, which is
|
|
141
|
+
// what overscan is for: a frame that has wandered must never be a frame
|
|
142
|
+
// with the page showing along one edge.
|
|
143
|
+
context.translate(middle.x + camera.swayX, middle.y + camera.swayY)
|
|
144
|
+
context.rotate(camera.tilt)
|
|
145
|
+
context.scale(camera.overscan, camera.overscan)
|
|
146
|
+
context.translate(-middle.x, -middle.y)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// What of the water is actually in the box, which a window on a larger
|
|
150
|
+
// water sees only a piece of. Nothing in the sea knows where the edge of
|
|
151
|
+
// the picture is, so this is the last chance not to pay for what is past
|
|
152
|
+
// it.
|
|
153
|
+
const seen: Seen = view
|
|
154
|
+
? {
|
|
155
|
+
foot: view.y + box.height / (2 * view.scale),
|
|
156
|
+
left: view.x - box.width / (2 * view.scale),
|
|
157
|
+
right: view.x + box.width / (2 * view.scale),
|
|
158
|
+
top: view.y - box.height / (2 * view.scale),
|
|
159
|
+
}
|
|
160
|
+
: { foot: box.height, left: 0, right: box.width, top: 0 }
|
|
161
|
+
|
|
162
|
+
room(reader.count)
|
|
163
|
+
|
|
164
|
+
// Where everything is, and which of the two it is in. Anything you can
|
|
165
|
+
// see the water through is laid over what is already there in the order
|
|
166
|
+
// it arrives, since two lights over one another are a painting rather
|
|
167
|
+
// than a stack of distances; everything else takes its turn by how far
|
|
168
|
+
// back it stands, which an infinite lane arranges.
|
|
169
|
+
let kept = 0
|
|
170
|
+
while (reader.next()) {
|
|
171
|
+
const through =
|
|
172
|
+
reader.alpha < 0.999 || reader.form === FORM.light || reader.form === FORM.wash
|
|
173
|
+
places[kept] = reader.offset
|
|
174
|
+
lanes[kept] = through ? Number.POSITIVE_INFINITY : reader.lane
|
|
175
|
+
kept++
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Stable, because equal lanes have to keep the order they were drawn in.
|
|
179
|
+
order.length = kept
|
|
180
|
+
for (let at = 0; at < kept; at++) order[at] = at
|
|
181
|
+
order.sort((a, b) => {
|
|
182
|
+
const one = lanes[a] ?? 0
|
|
183
|
+
const two = lanes[b] ?? 0
|
|
184
|
+
return one === two ? a - b : one - two
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const paint = new Reader(floats)
|
|
188
|
+
for (const which of order) {
|
|
189
|
+
paint.stand(places[which] ?? 0)
|
|
190
|
+
one(context, paint, palette, box, seen, bleed)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
context.setTransform(1, 0, 0, 1, 0, 0)
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** One drawing, whichever of the four kinds it is. */
|
|
199
|
+
function one(
|
|
200
|
+
context: CanvasRenderingContext2D,
|
|
201
|
+
mark: Reader,
|
|
202
|
+
palette: Palette,
|
|
203
|
+
box: Box,
|
|
204
|
+
seen: Seen,
|
|
205
|
+
bleed: number,
|
|
206
|
+
): void {
|
|
207
|
+
if (mark.alpha <= 0.002) return
|
|
208
|
+
|
|
209
|
+
if (mark.form === FORM.wash) {
|
|
210
|
+
context.fillStyle = down(context, mark, palette, box, 0, box.height)
|
|
211
|
+
context.fillRect(0, 0, box.width, box.height)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (mark.form === FORM.light) {
|
|
216
|
+
light(context, mark, palette, box)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const floats = mark.floats
|
|
221
|
+
const shape = new Path2D()
|
|
222
|
+
let top = Number.POSITIVE_INFINITY
|
|
223
|
+
let foot = Number.NEGATIVE_INFINITY
|
|
224
|
+
let left = Number.POSITIVE_INFINITY
|
|
225
|
+
let right = Number.NEGATIVE_INFINITY
|
|
226
|
+
|
|
227
|
+
mark.each((from, points) => {
|
|
228
|
+
if (points < 2) return
|
|
229
|
+
for (let i = 0; i < points; i++) {
|
|
230
|
+
const x = floats[from + i * 2] ?? 0
|
|
231
|
+
const y = floats[from + i * 2 + 1] ?? 0
|
|
232
|
+
if (i === 0) shape.moveTo(x, y)
|
|
233
|
+
else shape.lineTo(x, y)
|
|
234
|
+
if (y < top) top = y
|
|
235
|
+
if (y > foot) foot = y
|
|
236
|
+
if (x < left) left = x
|
|
237
|
+
if (x > right) right = x
|
|
238
|
+
}
|
|
239
|
+
if (mark.form !== FORM.stroke) shape.closePath()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
if (top > foot) return
|
|
243
|
+
|
|
244
|
+
// Off the picture entirely, which happens to a fish that has left and to
|
|
245
|
+
// every plant standing behind a mass of near rock.
|
|
246
|
+
const margin = Math.max(seen.right - seen.left, seen.foot - seen.top) * bleed
|
|
247
|
+
if (right < seen.left - margin || left > seen.right + margin) return
|
|
248
|
+
if (foot < seen.top - margin || top > seen.foot + margin) return
|
|
249
|
+
|
|
250
|
+
const tall = foot - top > box.height * TALL
|
|
251
|
+
const ink = tall
|
|
252
|
+
? down(context, mark, palette, box, top, foot)
|
|
253
|
+
: flat(mark, palette, box, mark.shade === OWN ? (top + foot) / 2 : mark.shade)
|
|
254
|
+
|
|
255
|
+
if (mark.form === FORM.stroke) {
|
|
256
|
+
context.strokeStyle = ink
|
|
257
|
+
context.lineWidth = Math.max(0.1, mark.width)
|
|
258
|
+
context.lineCap = "round"
|
|
259
|
+
context.lineJoin = "round"
|
|
260
|
+
context.stroke(shape)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
context.fillStyle = ink
|
|
265
|
+
context.fill(shape)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A round light, as an ellipse with the falling off built into a gradient.
|
|
270
|
+
*
|
|
271
|
+
* The card does this per pixel out of a distance from the middle. Here it is a
|
|
272
|
+
* radial gradient with the same curve sampled along it, which is the one place
|
|
273
|
+
* a canvas gets the better bargain: a gradient is a gradient however many
|
|
274
|
+
* pixels it covers.
|
|
275
|
+
*/
|
|
276
|
+
function light(context: CanvasRenderingContext2D, mark: Reader, palette: Palette, box: Box): void {
|
|
277
|
+
const floats = mark.floats
|
|
278
|
+
const cx = floats[mark.first + 1] ?? 0
|
|
279
|
+
const cy = floats[mark.first + 2] ?? 0
|
|
280
|
+
const across = mark.width
|
|
281
|
+
if (across <= 0) return
|
|
282
|
+
|
|
283
|
+
const hue = hueOf(
|
|
284
|
+
palette,
|
|
285
|
+
mark.tone,
|
|
286
|
+
mark.weight,
|
|
287
|
+
mark.shade === OWN ? cy : mark.shade,
|
|
288
|
+
box.height,
|
|
289
|
+
)
|
|
290
|
+
// Under 1 this is a streak rather than a disc, which is what a lens does with
|
|
291
|
+
// a light it cannot quite hold. The gradient is built inside the squash as
|
|
292
|
+
// well as the shape, since a gradient is read in whatever space it is filled.
|
|
293
|
+
const thin = Math.max(0.001, mark.thin)
|
|
294
|
+
|
|
295
|
+
context.save()
|
|
296
|
+
context.translate(cx, cy)
|
|
297
|
+
context.scale(1, thin)
|
|
298
|
+
|
|
299
|
+
const glow = context.createRadialGradient(0, 0, 0, 0, 0, across)
|
|
300
|
+
for (let stop = 0; stop < STOPS; stop++) {
|
|
301
|
+
const t = stop / (STOPS - 1)
|
|
302
|
+
glow.addColorStop(t, css(hue, mark.alpha * (1 - t) ** mark.fall))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
context.fillStyle = glow
|
|
306
|
+
context.beginPath()
|
|
307
|
+
context.arc(0, 0, across, 0, Math.PI * 2)
|
|
308
|
+
context.fill()
|
|
309
|
+
context.restore()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** One colour, read at one height. */
|
|
313
|
+
function flat(mark: Reader, palette: Palette, box: Box, y: number): string {
|
|
314
|
+
const hue = hueOf(palette, mark.tone, mark.weight, y, box.height)
|
|
315
|
+
return css(hue, leftAt(mark.alpha, mark.fadeTop, mark.fadeSpan, mark.fall, mark.spent, y))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The same, sampled down the drawing's own height. */
|
|
319
|
+
function down(
|
|
320
|
+
context: CanvasRenderingContext2D,
|
|
321
|
+
mark: Reader,
|
|
322
|
+
palette: Palette,
|
|
323
|
+
box: Box,
|
|
324
|
+
top: number,
|
|
325
|
+
foot: number,
|
|
326
|
+
): CanvasGradient {
|
|
327
|
+
const run = context.createLinearGradient(0, top, 0, Math.max(top + 1, foot))
|
|
328
|
+
|
|
329
|
+
for (let stop = 0; stop < STOPS; stop++) {
|
|
330
|
+
const t = stop / (STOPS - 1)
|
|
331
|
+
const y = top + (foot - top) * t
|
|
332
|
+
run.addColorStop(t, flat(mark, palette, box, mark.shade === OWN ? y : mark.shade))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return run
|
|
336
|
+
}
|
package/src/caustics.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light off a moving surface, landing on the ground.
|
|
3
|
+
*
|
|
4
|
+
* A wave is a lens. The top of the water is never flat, so the light coming
|
|
5
|
+
* through it is focused into bright lines and starved between them, and what
|
|
6
|
+
* lands on the sand is a net of them sliding about. It is the one thing that
|
|
7
|
+
* says a sea is lit from a surface rather than merely lighter towards the top.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this is a field and not a net
|
|
10
|
+
*
|
|
11
|
+
* A caustic drawn from above is the net everybody has seen. This water is seen
|
|
12
|
+
* from the side, and a floor seen edge on has no room for a net on it: what
|
|
13
|
+
* reaches the eye is the light on whatever faces upwards, which here is the
|
|
14
|
+
* ridges. So this is asked a place and answers how much light is on it, and the
|
|
15
|
+
* painter walks the ridges it has rather than laying a pattern over the box.
|
|
16
|
+
*
|
|
17
|
+
* ## Why the noise is ridged
|
|
18
|
+
*
|
|
19
|
+
* Smooth noise is cloud. What separates a caustic from a cloud is that it has
|
|
20
|
+
* lines in it: the focusing is sharp and the dark between two bright lines is
|
|
21
|
+
* darker than the average, so the thing to fold is the middle of the range up
|
|
22
|
+
* into a crease and then steepen it. Plain noise put through a curve stays a
|
|
23
|
+
* cloud however hard it is squeezed.
|
|
24
|
+
*/
|
|
25
|
+
import { makeNoise2 } from "./perlin.ts"
|
|
26
|
+
|
|
27
|
+
export interface Caustics {
|
|
28
|
+
/**
|
|
29
|
+
* How much light is on the ground at a place, 0 to 1.
|
|
30
|
+
*
|
|
31
|
+
* `across` is a share of the box and `depth` is how far forward the ground
|
|
32
|
+
* there is, so two ridges at the same x and different distances are lit
|
|
33
|
+
* differently. They have to be: the same pattern on every ridge would flatten
|
|
34
|
+
* the whole bed into one surface, which is the opposite of the errand.
|
|
35
|
+
*/
|
|
36
|
+
at(across: number, depth: number): number
|
|
37
|
+
step(seconds: number): void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CausticsOptions {
|
|
41
|
+
seed: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How many bright lines fit across the box, and across the water's distance. */
|
|
45
|
+
const CELLS = 7.5
|
|
46
|
+
|
|
47
|
+
const DEEP_CELLS = 2.4
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* How fast the whole pattern slides, in widths a second.
|
|
51
|
+
*
|
|
52
|
+
* Slow. The surface it comes off is moving quickly, but what a caustic does on
|
|
53
|
+
* the ground is mostly breathe: the lines wander and swell where they are
|
|
54
|
+
* rather than travelling, and a pattern crossing the picture at a rate reads as
|
|
55
|
+
* something being projected onto the sand.
|
|
56
|
+
*/
|
|
57
|
+
const DRIFT = 0.045
|
|
58
|
+
|
|
59
|
+
/** How narrow the bright lines are, and how sharply they give out. */
|
|
60
|
+
const CREASE = 2.1
|
|
61
|
+
|
|
62
|
+
const SHARP = 3.2
|
|
63
|
+
|
|
64
|
+
export function createCaustics(options: CausticsOptions): Caustics {
|
|
65
|
+
const noise = makeNoise2(options.seed ^ 0x6c1f)
|
|
66
|
+
let slid = 0
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
at(across: number, depth: number): number {
|
|
70
|
+
const held = noise(across * CELLS + slid, depth * DEEP_CELLS)
|
|
71
|
+
const crease = 1 - Math.abs(held) * CREASE
|
|
72
|
+
if (crease <= 0) return 0
|
|
73
|
+
|
|
74
|
+
return Math.min(1, crease) ** SHARP
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
step(seconds: number): void {
|
|
78
|
+
slid += seconds * DRIFT
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
}
|