Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
730b918
setup new chaos branch
katestange Apr 6, 2025
490a48b
setting up new branch
katestange Apr 6, 2025
57edaa6
warnings on corner/walker formulas
katestange Apr 6, 2025
5a9256d
ignore bad walker/corner results
katestange Apr 6, 2025
26424f9
fading
katestange Apr 8, 2025
451ad7f
code tweaks
katestange Apr 8, 2025
861a73c
revert so compiles
katestange Apr 9, 2025
143e79d
feat: Variables and their documentation
gwhitney Apr 9, 2025
abe7856
fix: wait for fonts to draw labels
gwhitney Apr 9, 2025
dbe6e3f
refactor: avoid taking cases on type of color formular return
gwhitney Apr 9, 2025
9d7a9f3
fix: correct step indexing to minimize full redraws
gwhitney Apr 10, 2025
e253efe
feat: try to record fading in chunks and re-fade smoothly
gwhitney Apr 10, 2025
525722e
fix: use float-based Framebuffer for smooth fade
gwhitney Oct 8, 2025
b5ced9c
fix: Framebuffer only for fading and don't draw stuff that fully fades
gwhitney Oct 8, 2025
0c095ca
fix: make sure alphas are transmitted to p5 at full precision
gwhitney Oct 9, 2025
1c0074a
fix: don't let those labels fade out on me
gwhitney Oct 9, 2025
966cca2
fix: avoid WebGL warnings about frustum when zoomed in
gwhitney Oct 9, 2025
fc88631
chore: don't compute corners twice just to outdent, fix lint
gwhitney Oct 10, 2025
e2d9a6c
fix: set max labels, don't mess with pixelsPerFrame
gwhitney Oct 10, 2025
c8b6918
chore: lint
gwhitney Oct 11, 2025
c0b159e
feat: add a static mode without chunking or framebuffer
gwhitney Oct 29, 2025
244995c
feat: add a position update formula
gwhitney Oct 29, 2025
faa515b
fix: ignore scroll wheel in static mode
gwhitney Oct 30, 2025
8e20b49
fix: correct last fix
gwhitney Oct 30, 2025
94f52f4
documentation cleaning
katestange Oct 30, 2025
f56b16d
documentation cleaning
katestange Oct 30, 2025
0908adc
documentation cleaning
katestange Oct 30, 2025
c03a531
Featured
katestange Nov 1, 2025
c83ed40
Featured bird
katestange Nov 1, 2025
9b10352
fix: go back to using framebuffer when fading
gwhitney Nov 2, 2025
662f6a1
chore: clean up FrameBuffer when Chaos is closed
gwhitney Nov 2, 2025
58040cf
finalize specimens
katestange Nov 13, 2025
752ac34
snapshot local
katestange Nov 13, 2025
0f91148
dummy for CI snapshots
katestange Nov 13, 2025
e91dd66
ci-snaps
katestange Nov 13, 2025
502280c
ci-snaps-yaml
katestange Nov 13, 2025
98a5a2a
lint fix
katestange Nov 13, 2025
94be66b
mysterious fix of semiliterate lint interplay
katestange Nov 15, 2025
78f46e7
testing updates
katestange Nov 16, 2025
0af2d39
ci snaps workflow
katestange Nov 17, 2025
2969464
ci snaps
katestange Nov 17, 2025
e283dc7
TAU
katestange Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion e2e/tests/featured.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test.describe('Featured gallery images', () => {
if (
featProps.visualizerKind === 'Histogram'
|| featProps.visualizerKind === 'Turtle'
|| featProps.visualizerKind === 'Chaos'
) {
details.tag = '@webGL'
}
Expand All @@ -22,7 +23,9 @@ test.describe('Featured gallery images', () => {
await page.goto(testURL)
await expect(
page.locator('#specimen-bar-desktop').getByText('play_arrow')
).toHaveId('pause-button', {timeout: 30000})
).toHaveId('pause-button', {
timeout: featProps.visualizerKind === 'Chaos' ? 60000 : 30000,
})
const matchParams =
browserName === 'firefox' && details.tag === '@webGL'
? {maxDiffPixelRatio: 0.01}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions e2e/tests/gallery.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ test.describe('Gallery', () => {
)
})
test('clicking on a featured item', async ({page}) => {
const danceCard = await page.locator('.card-body >> nth=2')
await expect(danceCard.locator('.titlespan')).toContainText(/Dance/)
const futileCard = await page.locator('.card-body >> nth=2')
await expect(futileCard.locator('.titlespan')).toContainText(/Futile/)
await page.locator('.card-body >> nth=2').click()
await expect(page.url()).not.toContain('gallery')
await expect(
await page.locator('#sequenceTab .item-name')
).toContainText(/163/)
).toContainText(/A000005/)
await expect(
await page.locator('#visualiserTab .item-name').innerText()
).toMatch('Mod Fill')
).toMatch('Chaos')
})
test('saving a specimen and then deleting it', async ({page}) => {
// test originally written when the following was the default:
Expand Down
6 changes: 4 additions & 2 deletions e2e/tests/transversal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test.describe('Visualizer-sequence challenges', () => {
for (const viz of vizKeys) {
const vizPar = viz === 'Chaos' ? 'circSize=5' : '' // ow tough to see
const details = {}
if (viz === 'Histogram') {
if (viz === 'Histogram' || viz === 'Turtle' || viz === 'Chaos') {
details.tag = '@webGL'
}
for (const seq of vizSeqs[viz]) {
Expand All @@ -80,7 +80,9 @@ test.describe('Visualizer-sequence challenges', () => {
page
.locator('#specimen-bar-desktop')
.getByText('play_arrow')
).toHaveId('pause-button', {timeout: 20000})
).toHaveId('pause-button', {
timeout: viz === 'Chaos' ? 60000 : 20000,
})
const matchParams =
browserName === 'firefox' && details.tag === '@webGL'
? {maxDiffPixelRatio: 0.02}
Expand Down
1 change: 1 addition & 0 deletions etc/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default tslint.config(
{allow: ['methods']},
],
'max-len': ['error', {code: 80, comments: 80}],
'no-irregular-whitespace': ['error', {skipComments: true}],
'no-trailing-spaces': 'error',
'no-undef': 'error',
'operator-linebreak': [
Expand Down
Binary file added src/assets/img/Chaos/doily.png
54 changes: 36 additions & 18 deletions src/shared/Chroma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,33 +172,51 @@ export function overlay(bot: Chroma, top: Chroma): Chroma {
const topa = retgl[ALPHA]
const botgl = bot.gl()
const bota = botgl[ALPHA] ?? 1.0
for (let c = 0; c <= ALPHA; ++c) {
let botval = botgl[c]
if (c < ALPHA) {
retgl[c] *= topa
botval *= bota
}
retgl[c] += botval * (1 - topa)
retgl[ALPHA] = bota * (1 - topa) + topa
for (let c = 0; c < ALPHA; ++c) {
retgl[c] =
(retgl[c] * topa + botgl[c] * bota * (1 - topa)) / retgl[ALPHA]
}
return chromaRaw(...retgl, 'gl')
}

/** md
#### ply(color: Chroma, plies: number)

Returns a newly-created Chroma object the same as _color_ except it represents
overlaying _plies_ many times with that _color_. Equivalently, the
chromatic components of _color_ are left alone, and the new opacity value
is given by 1 - (1 - alpha)^plies, where alpha is the opacity of _color_.
This operation is available in mathjs formulas using the ordinary
multiplication operator `*`, and in such formulas, the color and factor
may appear in either order. With these definitions and conventions, we have
such expected identities as `c + c == 2*c` and so on.

Combining the previous two operations in a linear combination in a
mathjs formula such as `red + x*chroma(blue, 0.01)` provides one reasonable
way to morph geometrically from `red` to `blue` as `x` goes from 1 to infinity,
but note that when the colors used as endpoints are on opposite sides of the
color wheel, the colors in the "middle" of this trajectory will be rather
greyish/muddy. See the [chroma-js api](https://www.vis4.net/chromajs/)
for other ways of creating color scales if direct alpha-compositing produces
undesirable results.
**/
export function ply(color: Chroma, plies: number) {
return chroma(color).alpha(1 - (1 - color.alpha()) ** plies)
}

/** md
#### dilute(color: Chroma, factor: number)

Returns a newly-created Chroma object the same as _color_ except that
its alpha value has been multiplied by _factor_. This operation is available
in mathjs formulas using the ordinary multiplication operator `*`, and in
such formulas, the color and factor may appear in either order.
its alpha value has been multiplied by _factor_.

Combining the previous two operations in a linear combination in a
mathjs formula such as `red + x*blue` provides one reasonable way to morph
smoothly from `red` to `blue` as `x` goes from 0 to 1, but note that when
the colors used as endpoints are on opposite sides of the color wheel, the
colors in the middle of this trajectory will be rather greyish/muddy. See the
[chroma-js api](https://www.vis4.net/chromajs/) for other ways of creating
color scales if direct alpha-compositing produces undesirable results.
**/
Combining this with addition as in `red + dilute(blue, x)` provides one
reasonable way to morph linearly from `red` to `blue` as `x` goes from
0 to 1, with the same caveats about greyish/muddy colors in the middle of
the trajectory.

**/
export function dilute(color: Chroma, factor: number) {
return chroma(color).alpha(factor * color.alpha())
}
25 changes: 23 additions & 2 deletions src/shared/__tests__/Chroma.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {describe, it, expect} from 'vitest'

import {chroma, rainbow, isChroma, overlay, dilute} from '../Chroma'
import {chroma, rainbow, isChroma, overlay, dilute, ply} from '../Chroma'

const red = chroma('red')

Expand Down Expand Up @@ -46,7 +46,14 @@ describe('overlay', () => {
})
it('does alpha-compositing', () => {
expect(overlay(chroma('red', 0.5), chroma('green', 0.5)).hex()).toBe(
'#404000bf'
'#555500bf'
)
})
it('is associative', () => {
const hg = chroma('green', 0.5)
const hb = chroma('blue', 0.5)
expect(overlay(overlay(red, hg), hb)).toStrictEqual(
overlay(red, overlay(hg, hb))
)
})
})
Expand All @@ -57,3 +64,17 @@ describe('dilute', () => {
expect(dilute(chroma('blue', 0.7), 0.6).alpha()).toBe(0.7 * 0.6)
})
})

describe('ply', () => {
it('represents several plies of the same overlay', () => {
const hc = chroma('cyan', 0.5)
expect(ply(hc, 2)).toStrictEqual(overlay(hc, hc))
const qc = ply(hc, 0.5)
const mhc = overlay(qc, qc)
const amhc = mhc.alpha(Math.round(mhc.alpha() * 2 ** 20) / 2 ** 20)
expect(amhc).toStrictEqual(hc)
})
it('has no effect on opaque colors', () => {
expect(ply(red, 3.5)).toStrictEqual(red)
})
})
27 changes: 16 additions & 11 deletions src/shared/__tests__/math.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {describe, it, expect} from 'vitest'

import {math} from '../math'

const large = 9007199254740993n
Expand Down Expand Up @@ -264,30 +263,36 @@ describe('colors', () => {
math.evaluate('chroma("blue") + chroma("yellow", 0.5)')
).toStrictEqual(chroma(0.5))
})
it('scalar multiplies a color via alpha', () => {
it('multiplies alpha with dilute', () => {
const g = chroma('lime')
expect(math.multiply(g, 0.5)).toStrictEqual(
chroma(0, 1, 0, 0.5, 'gl')
)
expect(math.dilute(g, 0.5)).toStrictEqual(chroma(0, 1, 0, 0.5, 'gl'))
// make sure g is not modified
expect(g.gl()).toStrictEqual([0, 1, 0, 1])
})
it('scalar multiplies consistent with addition', () => {
const hc = chroma('cyan', 0.3)
expect(math.evaluate('hc + hc', {hc})).toStrictEqual(
math.evaluate('2*hc', {hc})
)
})
it('provides a rainbow function', () => {
expect(math.rainbow(45).hex()).toBe('#ed2400')
expect(math.evaluate('rainbow(2+2i, 0.5)').hex()).toBe('#ed240080')
})
it('takes linear combinations in expressions', () => {
expect(
math.evaluate('chroma("blue") + 0.5*chroma("yellow")')
math.evaluate('chroma("blue") + dilute(chroma("yellow"),0.5)')
).toStrictEqual(chroma(0.5))
expect(
math.evaluate('0.5*chroma("blue") + 0.5*chroma("yellow")').gl()
).toStrictEqual([0.5, 0.5, 0.25, 0.75])
math
.evaluate('chroma("blue", 0.5) + 2*chroma("yellow", 0.5)')
.gl()
).toStrictEqual([6 / 7, 6 / 7, 1 / 7, 7 / 8])
})
it('allows direct use of color names in expressions', () => {
expect(math.evaluate('0.5*blue + 0.5*yellow').gl()).toStrictEqual([
0.5, 0.5, 0.25, 0.75,
])
expect(
math.evaluate('dilute(blue, 0.5) + dilute(yellow, 0.5)').gl()
).toStrictEqual([2 / 3, 2 / 3, 1 / 3, 0.75])
})
it('allows chroma.js operations in expressions', () => {
// examples from https://gka.github.io/chroma.js/
Expand Down
49 changes: 44 additions & 5 deletions src/shared/defineFeatured.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,24 @@ const featuredSIMs = [
+ '&colorChooser=813d9c'
),
specimenQuery(
'Divisor Square',
'Prime Jewels',
'Chaos',
'Formula',
'corners=4&walkers=2&sizeFormula=0.7'
+ '&colorFormula=%5B%23103547%2C%2375795E%2C%23962020'
+ '%2C%23D76533%2C%23385563%2C%23BDCAAE%2C%23DA9202'
+ '%2C%23612B39%5D%5Bmod%28A%28n%29-A%28n%2B8%29%2C8'
+ '%29%2B1%5D&pixelsPerFrame=200',
'formula=isPrime%28n%29*2%2BisPrime%28n%2B24%29'
+ '&last=99999&length=100000'
),
specimenQuery(
'Resistance is Futile',
'Chaos',
'OEIS A000005',
'corners=8&walkers=2&sizeFormula=0.6'
+ '&colorFormula=%5B%234D8E90%2C%23C6B06E%5D'
+ '%5Bmod%28w%2C2%29%2B1%5D&pixelsPerFrame=200',
'corners=8&walkers=8&alpha=0.7&pixelsPerFrame=2000'
),
specimenQuery(
Expand All @@ -39,6 +54,15 @@ const featuredSIMs = [
'Formula',
'fillColor=isPrime(n)+%3F+%23f661511a+%3A+%231a5fb41a'
),
specimenQuery(
'The Murmurations of the Punctual Bird',
'Chaos',
'OEIS A132131',
'corners=8&walkers=4&eagernessFormula=0.16&sizeFormula=2'
+ '&colorFormula=rainbow%28W*360%2Fh%29&pixelsPerFrame=3'
+ '&fadeEffect=0.015',
'first=1000&length=26349'
),
specimenQuery(
'Baffling Beatty Bars',
'ModFill',
Expand All @@ -65,12 +89,27 @@ const featuredSIMs = [
'formula=n%5E3%2B2n%2B1'
),
specimenQuery(
'Chaos Game',
'Doily-Dally',
'Chaos',
'Formula',
'corners=11&bgColor=1D0E0E03&staticMode=true'
+ '&cornerFormula=mod%28a%2Bc%2Cp%29'
+ '&sizeFormula=0.5&colorFormula=a%3F%235351BEFF%3A%231D5C98FF'
+ '&colorChooser=5351BEFF&pixelsPerFrame=500&fadeEffect=0.00',
'formula=pickRandom%28%5B+0%2C3%2C3%2C-3' + '%2C-3%5D%29'
),
specimenQuery(
'Barnsley Fern',
'Chaos',
'Random',
'corners=3&colorStyle=1&dummyDotControl=true'
+ '&circSize=2&alpha=0.4&darkMode=true',
'max=2'
'stepFormula=%28%28%5B0%2C0%3B0%2C.16%5D*%28a%3D%3D0%29+%2B+%5B.85'
+ '%2C.04%3B-.04%2C.85%5D+*%28a%3E2%29+%2B+%5B.2%2C-.26%3B.23'
+ '%2C.22%5D*%28a%3D%3D1%29+%2B++%5B-.15%2C.28%3B.26%2C.24%5D'
+ '*%28a%3D%3D2%29%29*%28P%2B%5B0%2C200%5D%29%2F40+%2B+%5B0'
+ '%2C1.6%5D*%28a%3D1+or+a%3E2%29+%2B+%5B0%2C.44%5D*%28a%3D'
+ '%3D2%29%29*40+-+%5B0%2C200%5D&sizeFormula=0.5'
+ '&colorFormula=%2300D132&colorChooser=00D132FF',
'max=7&last=99999&length=100000'
),
specimenQuery(
'Polyfactors',
Expand Down
13 changes: 9 additions & 4 deletions src/shared/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ import type {
import temml from 'temml'

import type {ValidationStatus} from './ValidationStatus'
import {chroma, isChroma, dilute, overlay, rainbow} from './Chroma'
import {chroma, isChroma, dilute, overlay, ply, rainbow} from './Chroma'
import type {Chroma} from './Chroma'

export type {MathType, MathNode, SymbolNode} from 'mathjs'
Expand Down Expand Up @@ -156,6 +156,7 @@ type ExtendedMathJs = Omit<MathJsInstance, 'hasNumericValue' | 'add'> & {
triangular<T extends Integer>(n: T): Widen<T>
invTriangular<T extends Integer>(n: T): Widen<T>
chroma: typeof chroma
dilute(c: Chroma, factor: number): Chroma
rainbow(a: Integer, opacity?: number): Chroma
isChroma(a: unknown): a is Chroma
add: ((c: Chroma, d: Chroma) => Chroma) &
Expand Down Expand Up @@ -186,6 +187,7 @@ math.typed.addType({

const colorStuff: Record<string, unknown> = {
chroma,
dilute: (c: Chroma, factor: number) => dilute(c, factor),
rainbow: (h: MathScalarType | bigint, opacity = 1) => {
if (math.isComplex(h)) h = (math.arg(h) * 180) / math.pi
else if (typeof h !== 'number' && typeof h !== 'bigint') {
Expand All @@ -196,8 +198,8 @@ const colorStuff: Record<string, unknown> = {
add: math.typed('add', {'Chroma, Chroma': (c, d) => overlay(c, d)}),
isChroma,
multiply: math.typed('multiply', {
'number, Chroma': (s, c) => dilute(c, s),
'Chroma, number': dilute,
'number, Chroma': (s, c) => ply(c, s),
'Chroma, number': ply,
}),
typeOf: math.typed('typeOf', {Chroma: () => 'Chroma'}),
boolean: math.typed('boolean', {
Expand Down Expand Up @@ -503,7 +505,10 @@ math.bigmin = (...args: Integer[]): ExtendedBigint => {
}

/* Helper for outputting scopes: */
type ScopeValue = MathType | Record<string, number> | ((x: never) => MathType)
export type ScopeValue =
| MathType
| Record<string, number>
| ((x: never) => MathType)
type ScopeType = Record<string, ScopeValue>
function scopeToString(scope: ScopeType) {
return Object.entries(scope)
Expand Down
Loading