From f9d2748067bc4f1cd78e5223eddf0c4e3763de59 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Mon, 3 Nov 2025 21:52:18 -0800 Subject: [PATCH 01/12] fix: don't post popup when mouse button is down --- doc/visualizer-in-depth.md | 8 ++ .../{Histogram.ts => FactorHistogram.ts} | 8 +- src/visualizers/P5Visualizer.ts | 111 +++++++++++------- 3 files changed, 81 insertions(+), 46 deletions(-) rename src/visualizers/{Histogram.ts => FactorHistogram.ts} (98%) diff --git a/doc/visualizer-in-depth.md b/doc/visualizer-in-depth.md index b8a9ef27..e3481a3f 100644 --- a/doc/visualizer-in-depth.md +++ b/doc/visualizer-in-depth.md @@ -298,6 +298,14 @@ fills it in from the `category`, and makes it read-only. ### Other properties +#### Status of mouse primary button + +A P5Visualizer automatically maintains a property `mousePrimaryDown` that is +true when the primary mouse button is in its pressed/down state. This property +is in essence identical to `sketch.mouseIsPressed` but (a) it specifically +only pays attention to the primary mouse button, and (b) has its value +maintained more reliably in the face of events outside the sketch. + #### Vue and reactive objects It is important to know that Vue will instrument (i.e., insert code into) your diff --git a/src/visualizers/Histogram.ts b/src/visualizers/FactorHistogram.ts similarity index 98% rename from src/visualizers/Histogram.ts rename to src/visualizers/FactorHistogram.ts index 94bb796d..babf1021 100644 --- a/src/visualizers/Histogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -267,8 +267,10 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { const xAxisHeight = largeOffsetScalar * sketch.height // Checks to see whether the mouse is in the bin drawn on the screen + // only trigger if no mouse button is pressed (e.g., not in midst + // of drag). let inBin = false - if (this.mouseOver) { + if (this.mouseOver && !this.mousePrimaryDown) { inBin = this.mouseOverInBin(xAxisHeight, binIndex) } @@ -381,14 +383,14 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.drawHoverBox(binIndex, smallOffsetNumber) } // Once everything is loaded, no need to redraw until mouse moves - if (!this.fontsLoaded || this.factoring || sketch.mouseIsPressed) { + if (!this.fontsLoaded || this.factoring || this.mousePrimaryDown) { this.continue() this.stop(3) } } mouseMoved() { - if (this.mouseOver || this.sketch.mouseIsPressed) { + if (this.mouseOver || this.mousePrimaryDown) { this.continue() this.stop(3) } diff --git a/src/visualizers/P5Visualizer.ts b/src/visualizers/P5Visualizer.ts index ad84f73a..5d14277f 100644 --- a/src/visualizers/P5Visualizer.ts +++ b/src/visualizers/P5Visualizer.ts @@ -41,7 +41,7 @@ class WithP5 extends Paramable { mouseDragged(_event: MouseEvent) {} mouseMoved(_event: MouseEvent) {} mousePressed(_event: MouseEvent) {} - mouseReleased() {} + mouseReleased(_event: MouseEvent) {} mouseWheel(_event: WheelEvent) {} touchEnded() {} touchMoved() {} @@ -71,11 +71,18 @@ export const INVALID_COLOR = {} as p5.Color /* Flag to force a call to presketch in a reset() call: */ export const P5ForcePresketch = true +/* Helper for mouse handling */ +function isPrimaryDown(e: MouseEvent) { + const flags = e.buttons ?? e.which + return (flags & 1) === 1 +} + export interface P5VizInterface extends VisualizerInterface, WithP5 { _sketch?: p5 _canvas?: p5.Renderer _framesRemaining: number size: ViewSize + mousePrimaryDown: boolean drawingState: DrawingState readonly sketch: p5 readonly canvas: p5.Renderer @@ -97,6 +104,7 @@ export function P5Visualizer(desc: PD) { _canvas?: p5.Renderer _framesRemaining = Infinity size = nullSize + mousePrimaryDown = false drawingState: DrawingState = DrawingUnmounted within?: HTMLElement @@ -160,60 +168,77 @@ export function P5Visualizer(desc: PD) { // `this[method]` is defined or undefined: for (const method of p5methods) { const definition = this[method] - if (definition !== dummyWithP5[method]) { - if ( - method === 'mousePressed' - || method === 'mouseClicked' - || method === 'mouseWheel' - ) { + const trivial = definition === dummyWithP5[method] + if ( + method === 'mousePressed' + || method === 'mouseClicked' + || method === 'mouseWheel' + ) { + if (trivial) { sketch[method] = (event: MouseEvent) => { - if (!this.within) return true - // Check that the event position is in bounds - const rect = - this.within.getBoundingClientRect() - const x = event.clientX - if (x < rect.left || x >= rect.right) { - return true - } - const y = event.clientY - if (y < rect.top || y >= rect.bottom) { - return true - } - // Check also that the event element is OK - const where = document.elementFromPoint(x, y) - if (!where) return true - if ( - where !== this.within + this.mousePrimaryDown = isPrimaryDown(event) + return true + } + continue + } + sketch[method] = (event: MouseEvent) => { + this.mousePrimaryDown = isPrimaryDown(event) + if (!this.within) return true + // Check that the event position is in bounds + const rect = this.within.getBoundingClientRect() + const x = event.clientX + if (x < rect.left || x >= rect.right) return true + const y = event.clientY + if (y < rect.top || y >= rect.bottom) return true + // Check also that the event element is OK + const where = document.elementFromPoint(x, y) + if (!where) return true + if ( + where !== this.within && !where.contains(this.within) - ) { - return true - } - return this[method](event as never) - // Cast makes typescript happy :-/ + ) { + return true + } + return this[method](event as never) + // Cast makes typescript happy :-/ + } + continue + } + if (method === 'mouseReleased' || method === 'mouseMoved') { + if (trivial) { + sketch[method] = (event: MouseEvent) => { + this.mousePrimaryDown = isPrimaryDown(event) + return true } continue } - if ( - method === 'keyPressed' + sketch[method] = (event: MouseEvent) => { + this.mousePrimaryDown = isPrimaryDown(event) + return this[method](event as never) + } + continue + } + if (trivial) continue + if ( + method === 'keyPressed' || method === 'keyReleased' || method === 'keyTyped' - ) { - sketch[method] = (event: KeyboardEvent) => { - const active = document.activeElement - if ( - active + ) { + sketch[method] = (event: KeyboardEvent) => { + const active = document.activeElement + if ( + active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA') - ) { - return true - } - return this[method](event) + ) { + return true } - continue + return this[method](event) } - // Otherwise no special condition, just forward event - sketch[method] = definition.bind(this) as () => void + continue } + // Otherwise no special condition, just forward event + sketch[method] = definition.bind(this) as () => void } // And draw is special because of the error handling: From 3ec0081ee4d61785e53af1e2f74a5b7cb207978f Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 4 Nov 2025 08:07:50 -0800 Subject: [PATCH 02/12] fix: correctly position labels when zooming --- src/visualizers/FactorHistogram.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index babf1021..4a95d124 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -249,7 +249,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { const {pX, scale} = this.mouseToPlot() sketch.textSize(Math.max(0.02 * sketch.height * scale, 10)) const height = this.height() // "unit" height - const textHeight = sketch.textAscent() * scale + const textHeight = sketch.textAscent() const largeOffsetScalar = 0.945 // padding between axes and edge const smallOffsetScalar = 0.996 const largeOffsetNumber = (1 - largeOffsetScalar) * sketch.width @@ -331,7 +331,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Draws the markings on the Y-axis const tickLeft = yAxisPosition - largeOffsetNumber / 5 const tickRight = yAxisPosition + largeOffsetNumber / 5 - const rightJustify = bigTickWidth < tickLeft - 2 * smallOffsetNumber + const rightJustify = bigTickWidth < tickLeft * scale - 2 * smallOffsetNumber for (let i = 1; i <= nTicks; i++) { // Draws the tick marks let tickY = xAxisHeight - tickHeight * height * i From 000318481f381a1292e11dd0d1d4c9e19a47f37b Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 4 Nov 2025 08:20:26 -0800 Subject: [PATCH 03/12] refactor: move factoring/bin setup to presketch to get a jump on it --- src/visualizers/FactorHistogram.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index 4a95d124..7c692b4c 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -109,7 +109,8 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Create an array with the frequency of each number // of factors in the corresponding bins - async binFactorArraySetup() { + async presketch() { + this.factoring = true await this.seq.fill(this.endIndex(), 'factors') const factorCount = this.factorCounts() let largestValue = factorCount.length - 1 @@ -168,8 +169,6 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.sketch.textFont(font) this.fontsLoaded = true }) - this.factoring = true - this.binFactorArraySetup() } barLabel(binIndex: number) { From 494632c0d102cbbbc888ef7825653484f955f34e Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 4 Nov 2025 08:38:10 -0800 Subject: [PATCH 04/12] fix: post warning about terms used for infinite sequence --- src/visualizers/FactorHistogram.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index 7c692b4c..f5953395 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -3,7 +3,7 @@ import {P5GLVisualizer} from './P5GLVisualizer' import interFont from '@/assets/fonts/inter/Inter-VariableFont_slnt,wght.ttf' import {math} from '@/shared/math' -import type {GenericParamDescription} from '@/shared/Paramable' +import type {GenericParamDescription, ParamValues} from '@/shared/Paramable' import {ParamType} from '@/shared/ParamType' import {ValidationStatus} from '@/shared/ValidationStatus' @@ -68,14 +68,20 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { numUnknown = 0 fontsLoaded = false + checkParameters(params: ParamValues) { + const status = super.checkParameters(params) + if (typeof this.seq.last !== 'bigint') { + status.addWarning('Using first 10000 terms of infinite sequence.') + } + return status + } + // Obtain the binned difference of an input binOf(input: number): number { return Math.trunc(input / this.binSize) } endIndex(): bigint { - // TODO: Should post warning about artificial limitation here - // (when it takes effect) return typeof this.seq.last === 'bigint' ? this.seq.last : this.seq.first + 9999n @@ -86,7 +92,8 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // are put into -1 factorCounts(): number[] { const factorCount = [] - for (let i = this.seq.first; i <= this.endIndex(); i++) { + const last = this.endIndex() + for (let i = this.seq.first; i <= last; i++) { let counter = 0 const factors = this.seq.getFactors(i) if (factors) { From f94b4d064c1911ff1699d3c9492e4c029b3b4106 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 4 Nov 2025 16:05:06 -0800 Subject: [PATCH 05/12] fix: don't block the browser when the number of terms is huge --- doc/visualizer-in-depth.md | 27 +++++++---- src/components/SwitcherModal.vue | 2 +- src/sequences/Cached.ts | 33 +++++++------ src/sequences/OEIS.ts | 2 +- src/sequences/SequenceInterface.ts | 8 ++++ ...layoutUtilities.spec.ts => layout.spec.ts} | 2 +- src/shared/asynchronous.ts | 9 ++++ src/shared/{layoutUtilities.ts => layout.ts} | 0 src/views/Scope.vue | 2 +- .../P5VisualizerTemplate.ts | 4 +- src/visualizers/FactorFence.ts | 17 +++++-- src/visualizers/FactorHistogram.ts | 30 ++++++++---- src/visualizers/ModFill.ts | 10 ++-- src/visualizers/NumberGlyph.ts | 47 +++++++++++-------- src/visualizers/P5Visualizer.ts | 30 +++++++----- 15 files changed, 143 insertions(+), 80 deletions(-) rename src/shared/__tests__/{layoutUtilities.spec.ts => layout.spec.ts} (94%) create mode 100644 src/shared/asynchronous.ts rename src/shared/{layoutUtilities.ts => layout.ts} (100%) diff --git a/doc/visualizer-in-depth.md b/doc/visualizer-in-depth.md index e3481a3f..696a67f9 100644 --- a/doc/visualizer-in-depth.md +++ b/doc/visualizer-in-depth.md @@ -398,17 +398,20 @@ opportunity to do pre-computation as well. That is the `presketch()` method, which runs asynchronously, meaning that the browser will not be blocked while this function completes. This facility is not a part of p5.js, but a part of the P5Visualizer design. The `presketch()` method is called by the framework -with one argument, representing the size of the canvas to be created as a -ViewSize object with number fields `width` and `height`. +with two boolean arguments: the first specifies whether the sequence has +changed since the last `presketch()`, and the second specifies whether the size +has changed. (Both arguments are true for initialization.) You can obtain the +size of the canvas to be created via the `this.size` property, a ViewSize +object with number fields `width` and `height`. If you implement `presketch()`, begin by calling -`await super.presketch(size)`, which will initialize the sequence that the -visualizer is viewing. After this call, you have access to the values of the -sequence, so you can do sequence-dependent initialization here. It is OK to -set up internal data variables in this method. For example, this is a good -place to populate an array with time-consuming precomputed values you will use -repeatedly during the sketch. However, in `presketch()` you still have no -access to the p5 canvas or the `this.sketch` object. +`await super.presketch(seqChanged, sizeChanged)`, which will initialize the +sequence that the visualizer is viewing. After this call, you have access to +the values of the sequence, so you can do sequence-dependent initialization +here. It is OK to set up internal data variables in this method. For example, +this is a good place to populate an array with time-consuming precomputed +values you will use repeatedly during the sketch. However, in `presketch()` +you still have no access to the p5 canvas or the `this.sketch` object. Note also that `presketch()` is called when there is a new visualizer, when the sequence changes, when the canvas size changes, and when you reload the @@ -417,6 +420,12 @@ parameters change. So if there is initialization you want to do only on these more signifcant changes but not on parameter changes, then `presketch()` is a good method. +Note that since `presketch()` is called asynchronously, you cannot assume it +has completed by the time any other method has been called, e.g. `setup()` +or `draw()`. Therefore, P5Visualizer provides a boolean property +`this.presketchComplete` that you can test to see if the presketch() +initialization is done. + When a visualizer is resized, or the restart button on Numberscope is pressed, the class function `reset()` is called. By default, a new canvas is created on a `reset()` (and so `setup()` will be called). In this event, `presketch()` diff --git a/src/components/SwitcherModal.vue b/src/components/SwitcherModal.vue index cd21c30e..9cc0cb1c 100644 --- a/src/components/SwitcherModal.vue +++ b/src/components/SwitcherModal.vue @@ -77,7 +77,7 @@ click on the trash button on its preview card. addSequence, deleteSequence, } from '@/shared/browserCaching' - import {isMobile} from '@/shared/layoutUtilities' + import {isMobile} from '@/shared/layout' import {Specimen} from '@/shared/Specimen' import { specimenQuery, diff --git a/src/sequences/Cached.ts b/src/sequences/Cached.ts index a91f97d0..65d9ffcb 100644 --- a/src/sequences/Cached.ts +++ b/src/sequences/Cached.ts @@ -1,6 +1,7 @@ import type {Factorization, SequenceInterface} from './SequenceInterface' import simpleFactor from './simpleFactor' +import {yieldExecution} from '@/shared/asynchronous' import {math, CachingError} from '@/shared/math' import type {ExtendedBigint} from '@/shared/math' import {Paramable, paramClone} from '@/shared/Paramable' @@ -151,7 +152,7 @@ export function Cached(desc: PD) { firstValueCached: ExtendedBigint = math.posInfinity lastFactorCached = -1024n // dummy value firstFactorCached: ExtendedBigint = math.posInfinity - valueCachingPromise = Promise.resolve() + valueCachingPromise: Promise | undefined = undefined factorCachingPromise = Promise.resolve() /** @@ -256,6 +257,7 @@ export function Cached(desc: PD) { async fillValueCache(n: bigint) { const start = this.lastValueCached + 1n for (let i = start; i <= n; i++) { + if (i % 10000n == 0n) await yieldExecution() const key = i.toString() // trust values we find; hopefully we have cleared when // needed, so that we can presume they are from some @@ -269,16 +271,13 @@ export function Cached(desc: PD) { } async cacheValues(n: bigint) { - // Let any existing value caching complete - await this.valueCachingPromise - // Let any pending parameter changes complete if (this.parChangePromise) await this.parChangePromise + if (this.valueCachingPromise) await this.valueCachingPromise if (n > this.lastValueCached) { this.valueCachingPromise = this.fillValueCache(n) await this.valueCachingPromise - } else { - this.valueCachingPromise = Promise.resolve() } + this.valueCachingPromise = undefined } getElement(n: bigint): bigint { @@ -299,6 +298,8 @@ export function Cached(desc: PD) { await this.cacheValues(n) const start = this.lastFactorCached + 1n for (let i = start; i <= n; ++i) { + // Can we yield execution? + if (i % 10000n == 0n) await yieldExecution() const key = i.toString() // trust values we find if (!(key in this.factorCache)) { @@ -392,11 +393,13 @@ export function Cached(desc: PD) { // patchy; all we can rely on is that the closed // interval from the _previous_ value of `this.first` // to `this.lastValueCached` is full. - if ( - this.first < this.firstValueCached - || this.first > this.lastValueCached + 1n + if (!needsReset + && (this.first < this.firstValueCached + || this.first > this.lastValueCached + 1n) ) { - await this.valueCachingPromise + if (this.valueCachingPromise) { + await this.valueCachingPromise + } this.firstValueCached = math.posInfinity this.lastValueCached = this.first - 1n // Get the new cache rolling: @@ -405,9 +408,9 @@ export function Cached(desc: PD) { // And then ditto for factoring. Here we will not // kick off the factoring process because we don't // even know if this sequence is being factored. - if ( - this.first < this.firstFactorCached - || this.first > this.lastFactorCached + 1n + if (!needsReset + && (this.first < this.firstFactorCached + || this.first > this.lastFactorCached + 1n) ) { await this.factorCachingPromise this.firstFactorCached = math.posInfinity @@ -441,7 +444,9 @@ export function Cached(desc: PD) { } if (needsReset) { this.ready = false - await this.valueCachingPromise + if (this.valueCachingPromise) { + await this.valueCachingPromise + } await this.factorCachingPromise this.initialize() } diff --git a/src/sequences/OEIS.ts b/src/sequences/OEIS.ts index 1ac1ea71..eb7ebad1 100644 --- a/src/sequences/OEIS.ts +++ b/src/sequences/OEIS.ts @@ -5,7 +5,7 @@ import type {Factorization} from './SequenceInterface' import simpleFactor from './simpleFactor' import {alertMessage} from '@/shared/alertMessage' -import {breakableString} from '@/shared/layoutUtilities' +import {breakableString} from '@/shared/layout' import {math} from '@/shared/math' import type {ExtendedBigint} from '@/shared/math' import type {GenericParamDescription} from '@/shared/Paramable' diff --git a/src/sequences/SequenceInterface.ts b/src/sequences/SequenceInterface.ts index fbe779be..333ea092 100644 --- a/src/sequences/SequenceInterface.ts +++ b/src/sequences/SequenceInterface.ts @@ -32,6 +32,14 @@ export interface SequenceInterface extends ParamableInterface { */ readonly length: ExtendedBigint + /** + * HACK: We make the last factor cached accessible for the sake of + * showing progress information on long caching runs. + * TODO: add a 'progress' function or something like that to do such + * things in a more disciplined and flexible way. + */ + readonly lastFactorCached: bigint + /** * Initialize is called after validation. It allows us to wait * until all the parameters are appropriate before we actually diff --git a/src/shared/__tests__/layoutUtilities.spec.ts b/src/shared/__tests__/layout.spec.ts similarity index 94% rename from src/shared/__tests__/layoutUtilities.spec.ts rename to src/shared/__tests__/layout.spec.ts index 1f0ed57b..7009f0f7 100644 --- a/src/shared/__tests__/layoutUtilities.spec.ts +++ b/src/shared/__tests__/layout.spec.ts @@ -1,6 +1,6 @@ import {describe, it, expect} from 'vitest' -import {breakableString} from '../layoutUtilities' +import {breakableString} from '../layout' // isMobile not easily unit testable, since it needs a browser context describe('breakableString', () => { diff --git a/src/shared/asynchronous.ts b/src/shared/asynchronous.ts new file mode 100644 index 00000000..362987f0 --- /dev/null +++ b/src/shared/asynchronous.ts @@ -0,0 +1,9 @@ +/* Helper functions for dealing ith JavaScript's asynchronous system */ + +/* Returns a trivial Promise. The point is that if you periodically await + * the result of this function, it emulates, at least to some extent, + * cooperative multitasking. + */ +export function yieldExecution() { + return new Promise(res => setTimeout(res, 0)) +} diff --git a/src/shared/layoutUtilities.ts b/src/shared/layout.ts similarity index 100% rename from src/shared/layoutUtilities.ts rename to src/shared/layout.ts diff --git a/src/views/Scope.vue b/src/views/Scope.vue index 3627ac37..3b4087f6 100644 --- a/src/views/Scope.vue +++ b/src/views/Scope.vue @@ -165,7 +165,7 @@ visualizers you can select. import NavBar from './minor/NavBar.vue' import SpecimenBar from '../components/SpecimenBar.vue' import {getCurrent, updateCurrent} from '@/shared/browserCaching' - import {isMobile} from '@/shared/layoutUtilities' + import {isMobile} from '@/shared/layout' /** * Positions a tab to be inside a dropzone diff --git a/src/visualizers-workbench/P5VisualizerTemplate.ts b/src/visualizers-workbench/P5VisualizerTemplate.ts index fecc51fb..6f2ad209 100644 --- a/src/visualizers-workbench/P5VisualizerTemplate.ts +++ b/src/visualizers-workbench/P5VisualizerTemplate.ts @@ -113,14 +113,14 @@ class P5VisualizerTemplate extends P5Visualizer(paramDesc) { textColor = INVALID_COLOR outlineColor = INVALID_COLOR - async presketch(size: ViewSize) { + async presketch(seqChanged: boolean, sizeChanged: boolean) { // === Asynchronous setup === // If any pre-computations must be run before the sketch is created, // placing them in the `presketch()` function will allow them // to run asynchronously, i.e. without blocking the browser. // The sketch will not be created until this function completes. - await super.presketch(size) + await super.presketch(seqChanged, sizeChanged) // The above call performs the default behavior of intializing the // first cache block of the sequence. // So down here is where you can do any computation-heavy preparation diff --git a/src/visualizers/FactorFence.ts b/src/visualizers/FactorFence.ts index 21780d98..a0a1a2af 100644 --- a/src/visualizers/FactorFence.ts +++ b/src/visualizers/FactorFence.ts @@ -338,14 +338,14 @@ class FactorFence extends P5Visualizer(paramDesc) { this.collectDataForScale(barsInfo, size) } - async presketch(size: ViewSize) { - await super.presketch(size) + async presketch(seqChange: boolean, sizeChange: boolean) { + await super.presketch(seqChange, sizeChange) // Warn the backend we plan to factor: (Note we don't await because // we don't actually use the factors until later.) this.seq.fill(this.seq.first + this.initialLimitTerms, 'factor') - await this.standardizeView(size) + await this.standardizeView(this.size) } setup() { @@ -475,6 +475,17 @@ In addition, several keypress commands are recognized: this.sketch.clear(0, 0, 0, 0) this.sketch.background(this.palette.backgroundColor) + // If we are still initializing, just display a message + if (!this.presketchComplete) { + this.sketch + .fill('red') + .text( + 'Scanning data to set scale...', + this.size.width/2, + this.size.height/2) + return + } + // determine which terms will be on the screen so we only // bother with those const barsInfo = this.barsShowing({ diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index f5953395..a47d8783 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -2,6 +2,7 @@ import {VisualizerExportModule} from './VisualizerInterface' import {P5GLVisualizer} from './P5GLVisualizer' import interFont from '@/assets/fonts/inter/Inter-VariableFont_slnt,wght.ttf' +import {yieldExecution} from '@/shared/asynchronous' import {math} from '@/shared/math' import type {GenericParamDescription, ParamValues} from '@/shared/Paramable' import {ParamType} from '@/shared/ParamType' @@ -63,7 +64,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { static description = 'Displays a histogram of the number of prime factors of a sequence' - factoring = true + precomputing = '' binFactorArray: number[] = [] numUnknown = 0 fontsLoaded = false @@ -90,10 +91,11 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Create an array with the value at n being the number of entries // of the sequence having n factors. Entries with unknown factorization // are put into -1 - factorCounts(): number[] { + async factorCounts(): Promise { const factorCount = [] const last = this.endIndex() for (let i = this.seq.first; i <= last; i++) { + if (i % 10000n === 0n) await yieldExecution() let counter = 0 const factors = this.seq.getFactors(i) if (factors) { @@ -116,10 +118,17 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Create an array with the frequency of each number // of factors in the corresponding bins - async presketch() { - this.factoring = true + async presketch(seqChanged: boolean, sizeChanged: boolean) { + this.precomputing = 'Evaluating sequence entries ...' + await super.presketch(seqChanged, sizeChanged) + if (!seqChanged) { + this.precomputing = '' + return + } + this.precomputing = 'Factoring ...' await this.seq.fill(this.endIndex(), 'factors') - const factorCount = this.factorCounts() + this.precomputing = 'Collecting values ...' + const factorCount = await this.factorCounts() let largestValue = factorCount.length - 1 if (largestValue < 0) largestValue = 0 this.binFactorArray = new Array(this.binOf(largestValue) + 1).fill(0) @@ -130,7 +139,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.numUnknown = factorCount[-1] this.binFactorArray[0] += this.numUnknown } else this.numUnknown = 0 - this.factoring = false + this.precomputing = '' } // Create a number that represents how @@ -261,9 +270,12 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { const largeOffsetNumber = (1 - largeOffsetScalar) * sketch.width const smallOffsetNumber = (1 - smallOffsetScalar) * sketch.width - if (this.factoring) { + if (this.precomputing) { sketch.fill('red') - this.write('Factoring ...', largeOffsetNumber, textHeight * 2) + this.write( + `${this.precomputing} ${this.seq.lastFactorCached}`, + largeOffsetNumber, + textHeight * 2) this.continue() this.stop(3) } @@ -389,7 +401,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.drawHoverBox(binIndex, smallOffsetNumber) } // Once everything is loaded, no need to redraw until mouse moves - if (!this.fontsLoaded || this.factoring || this.mousePrimaryDown) { + if (!this.fontsLoaded || this.precomputing || this.mousePrimaryDown) { this.continue() this.stop(3) } diff --git a/src/visualizers/ModFill.ts b/src/visualizers/ModFill.ts index eb298651..6f22a4d1 100644 --- a/src/visualizers/ModFill.ts +++ b/src/visualizers/ModFill.ts @@ -202,16 +202,12 @@ class ModFill extends P5Visualizer(paramDesc) { return this.aspectRatio == true ? 1 : undefined } - async presketch(size: ViewSize) { - await super.presketch(size) - const minDimension = Math.min(size.width, size.height) + setup() { + super.setup() + const minDimension = Math.min(this.size.width, this.size.height) // 16 was chosen in the following expression by doubling the // multiplier until the traces were almost too faint to see at all. this.maxModulus = 16 * minDimension - } - - setup() { - super.setup() const modDimWarning = 'Running with maximum modulus' // We need to check if the "mod dimension" fits on screen, diff --git a/src/visualizers/NumberGlyph.ts b/src/visualizers/NumberGlyph.ts index 90dde644..abb43cd7 100644 --- a/src/visualizers/NumberGlyph.ts +++ b/src/visualizers/NumberGlyph.ts @@ -169,13 +169,13 @@ class NumberGlyph extends P5Visualizer(paramDesc) { this.last = this.seq.first + this.n - 1n } - async presketch(size: ViewSize) { - await super.presketch(size) - this.adjustTermsAndColumns(size) - // NumberGlyph needs access to its entire range of values - // before the sketch setup is even called - await this.seq.fill(this.last, 'factors') + async presketch(seqChanged: boolean, sizeChanged: boolean) { + await super.presketch(seqChanged, sizeChanged) + this.adjustTermsAndColumns(this.size) + if (!seqChanged) return + + await this.seq.fill(this.last, 'factors') // Obtain all prime numbers that appear as factors in the sequence for (let i = this.seq.first; i < this.last; i++) { const checkCurrentFactors = this.seq.getFactors(i) @@ -213,33 +213,30 @@ class NumberGlyph extends P5Visualizer(paramDesc) { setup() { super.setup() - this.adjustTermsAndColumns(this.size) this.currentIndex = this.seq.first this.position = this.sketch.createVector(0, 0) - this.initialRadius = Math.floor(this.positionIncrement / 2) - this.radii = this.initialRadius this.sketch .background('black') .colorMode(this.sketch.HSB, 360, 100, 100) .frameRate(30) - - // Set position of the circle - this.initialPosition = this.sketch.createVector( - this.initialRadius, - this.initialRadius - ) - this.position = this.initialPosition.copy() } draw() { + if (!this.presketchComplete) { + this.sketch + .fill('red') + .text( + 'Factoring...', this.size.width / 2, this.size.height / 2) + return + } + if (this.changePosition()) this.sketch.background('black') this.sketch.noStroke() if (this.currentIndex > this.last) { this.stop() return } this.drawCircle(this.currentIndex) - this.changePosition() ++this.currentIndex } @@ -301,8 +298,19 @@ class NumberGlyph extends P5Visualizer(paramDesc) { } } - changePosition() { - this.position.add(this.positionIncrement, 0) + // returns true on first draw, false otherwise + changePosition(): boolean { + this.initialRadius = Math.floor(this.positionIncrement / 2) + this.radii = this.initialRadius + let first = false + if (this.position.x === 0) { + first = true + this.initialPosition = this.sketch.createVector( + this.initialRadius, + this.initialRadius + ) + this.position = this.initialPosition.copy() + } else this.position.add(this.positionIncrement, 0) // if we need to go to next line if ( math.divides( @@ -313,6 +321,7 @@ class NumberGlyph extends P5Visualizer(paramDesc) { this.position.x = this.initialPosition.x this.position.add(0, this.positionIncrement) } + return first } isPrime(ind: bigint): boolean { diff --git a/src/visualizers/P5Visualizer.ts b/src/visualizers/P5Visualizer.ts index 5d14277f..94d1c5cc 100644 --- a/src/visualizers/P5Visualizer.ts +++ b/src/visualizers/P5Visualizer.ts @@ -87,8 +87,9 @@ export interface P5VizInterface extends VisualizerInterface, WithP5 { readonly sketch: p5 readonly canvas: p5.Renderer seq: SequenceInterface + presketchComplete: boolean _initializeSketch(): (sketch: p5) => void - presketch(size: ViewSize): Promise + presketch(sequenceChanged: boolean, sizeChanged: boolean): Promise hatchRect(x: number, y: number, w: number, h: number): void reset(): Promise } @@ -106,6 +107,7 @@ export function P5Visualizer(desc: PD) { size = nullSize mousePrimaryDown = false drawingState: DrawingState = DrawingUnmounted + presketchComplete = false within?: HTMLElement popup?: HTMLElement = undefined @@ -284,25 +286,27 @@ export function P5Visualizer(desc: PD) { * The width and height the visualizer should occupy */ async inhabit(element: HTMLElement, size: ViewSize) { - let needsPresketch = true + let sequenceChanged = true + let sizeChanged = true if (this.within) { // oops, already inhabiting somewhere else; depart there this.depart(this.within) - // Only do the presketch initialization if the size has - // changed, though: - needsPresketch = + sequenceChanged = false // viewing a new sequence departs + sizeChanged = size.width !== this.size.width || size.height !== this.size.height } this.size = size this.within = element - // Perform any necessary asynchronous preparation before + // Initiate any necessary asynchronous preparation before // creating sketch. For example, some Visualizers need sequence - // factorizations in setup(). - if (needsPresketch) await this.presketch(size) - // TODO: Can presketch() sometimes take so long that we should - // show an hourglass icon in the meantime, or something like that? - + // factorizations, which are expensive to compute. + if (sequenceChanged || sizeChanged) { + this.presketchComplete = false + this.presketch(sequenceChanged, sizeChanged).then(() => { + this.presketchComplete = true + }) + } // Now we can create the sketch this._sketch = new p5(this._initializeSketch(), element) } @@ -312,8 +316,8 @@ export function P5Visualizer(desc: PD) { * things that must happen asynchronously before a p5 visualizer * can create its sketch. */ - async presketch(_size: ViewSize) { - await this.seq.fill() + async presketch(seqChange: boolean, _sizeChange: boolean) { + if (seqChange) await this.seq.fill() } /** From 7ec6e3759cad9baf0a609ef43250be6934de78a7 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 4 Nov 2025 16:33:14 -0800 Subject: [PATCH 06/12] doc: explain hashing of first bar --- src/visualizers/FactorHistogram.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index a47d8783..b1a820c6 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -26,8 +26,11 @@ Omega(n),_number_of_prime_factors_of_n_(with_multiplicity)). The horizontal axis represents values of Omega. Each bar corresponds to a range of possible Omega values (a bin). The height of each bar shows how many entries in the sequence -have a corresponding value of Omega. - +have a corresponding value of Omega. The leftmost bar corresponding to 0 +factors counts all sequence entries equal to 0 or 1, as well as those entries +that could not be factored (because their values were too large for Numberscope +to handle). The latter are indicated by hashing a portion of that leftmost bar +proportional to the number of unfactored entries. ## Parameters **/ From c9ab4e08685e2082a6243161ec0f4536ca571633 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Tue, 25 Nov 2025 19:44:41 -0800 Subject: [PATCH 07/12] test: changes to ensure full suite of tests pass --- .github/workflows/ci.yaml | 12 ++--- doc/visualizer-in-depth.md | 10 ++--- e2e/tests/featured.spec.ts | 2 +- e2e/tests/transversal.spec.ts | 6 ++- .../FactorHistogramA001220-chromium-linux.png | Bin 0 -> 4251 bytes .../FactorHistogramA001220-firefox-linux.png | Bin 0 -> 16996 bytes .../FactorHistogramA002819-chromium-linux.png | Bin 0 -> 11959 bytes .../FactorHistogramA002819-firefox-linux.png | Bin 0 -> 30150 bytes src/sequences/Cached.ts | 6 ++- src/shared/defineFeatured.ts | 2 +- .../P5VisualizerTemplate.ts | 1 - src/visualizers/FactorFence.ts | 5 ++- src/visualizers/FactorHistogram.ts | 6 ++- src/visualizers/ModFill.ts | 1 - src/visualizers/NumberGlyph.ts | 41 ++++++++++-------- src/visualizers/P5Visualizer.ts | 15 ++++--- 16 files changed, 61 insertions(+), 46 deletions(-) create mode 100644 e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-chromium-linux.png create mode 100644 e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-firefox-linux.png create mode 100644 e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-chromium-linux.png create mode 100644 e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-firefox-linux.png diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e79c6bc5..58fede86 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,9 +49,9 @@ jobs: - name: End-to-end test run: npx playwright test -c e2e/playci.config.ts ### Uncomment when you need to re-extract snapshots -# - name: Extract snapshots -# if: always() -# uses: actions/upload-artifact@v4 -# with: -# name: ci_actual_snapshots -# path: e2e/results/manual/output + - name: Extract snapshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: ci_actual_snapshots + path: e2e/results/manual/output diff --git a/doc/visualizer-in-depth.md b/doc/visualizer-in-depth.md index 696a67f9..697d4feb 100644 --- a/doc/visualizer-in-depth.md +++ b/doc/visualizer-in-depth.md @@ -399,9 +399,9 @@ which runs asynchronously, meaning that the browser will not be blocked while this function completes. This facility is not a part of p5.js, but a part of the P5Visualizer design. The `presketch()` method is called by the framework with two boolean arguments: the first specifies whether the sequence has -changed since the last `presketch()`, and the second specifies whether the size -has changed. (Both arguments are true for initialization.) You can obtain the -size of the canvas to be created via the `this.size` property, a ViewSize +changed since the last `presketch()`, and the second specifies whether the +size has changed. (Both arguments are true for initialization.) You can obtain +the size of the canvas to be created via the `this.size` property, a ViewSize object with number fields `width` and `height`. If you implement `presketch()`, begin by calling @@ -421,8 +421,8 @@ more signifcant changes but not on parameter changes, then `presketch()` is a good method. Note that since `presketch()` is called asynchronously, you cannot assume it -has completed by the time any other method has been called, e.g. `setup()` -or `draw()`. Therefore, P5Visualizer provides a boolean property +has completed by the time any other method has been called, e.g. `setup()` or +`draw()`. Therefore, P5Visualizer provides a boolean property `this.presketchComplete` that you can test to see if the presketch() initialization is done. diff --git a/e2e/tests/featured.spec.ts b/e2e/tests/featured.spec.ts index 7bce2a62..d8ccdbf9 100644 --- a/e2e/tests/featured.spec.ts +++ b/e2e/tests/featured.spec.ts @@ -9,7 +9,7 @@ test.describe('Featured gallery images', () => { const featProps = parseSpecimenQuery(feature.query) const details = {} if ( - featProps.visualizerKind === 'Histogram' + featProps.visualizerKind === 'FactorHistogram' || featProps.visualizerKind === 'Turtle' || featProps.visualizerKind === 'Chaos' ) { diff --git a/e2e/tests/transversal.spec.ts b/e2e/tests/transversal.spec.ts index a8d9b3fa..4dcdc4bd 100644 --- a/e2e/tests/transversal.spec.ts +++ b/e2e/tests/transversal.spec.ts @@ -69,7 +69,11 @@ 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' || viz === 'Turtle' || viz === 'Chaos') { + if ( + viz === 'FactorHistogram' + || viz === 'Turtle' + || viz === 'Chaos' + ) { details.tag = '@webGL' } for (const seq of vizSeqs[viz]) { diff --git a/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-chromium-linux.png b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..64c31df15931c81e2a0ad7085ea1f0d890be1ade GIT binary patch literal 4251 zcmeHKZB)`%7{6#~t5|NWthpd#X)iM^o!}JRjB*}dn67CZXXt2HC*KN_Pz08yxr*MV zLld=is1xBa#V>O1}K5 zB9K<&tM2pO)g0qV3J@JvL#OuSqTuu#C&{@eI6cP|S|kAPy)C%(vSa6kfDBCyF@;AP zUYe9M-#tzMV4ZIZPjkb)z29IOzoOvHHkkjSSxth*LIAw-Zq9caZ)T^i7fkEW3!_pD z1{2mA`w2d=_Yq$_^IXE=PEipcp)Wj}(S=WYI;bxeTAzunGlmQ_{4u)^_scEKo?QbL zZL5E30DZV5gfUs8VM+1_(ztW}$&XXiP}BX;VK1h-)V}nqkqC_V$jHI(xtcf0VPN5wp$PRBvV2UraS){LHG)KhW*5^xdO z4Do#LRauF(_ERz41j&hmGv=d$gRGP~;u63f@ljvk4bq2-3Q)|~<~zlj`UjI!NK!mw zB%@*l01^uP$~pnMYzhOwD{HW}9Sgul5du_VVA#0H2DTd5`oJ~@HY#`%OnAA~SS=-O zzhDp8*ScGbIm(zEWe+h`sBcv*YTLFb1|KIki8YMb!LS`iIdGWk2Q9{D{u=RAwPR_` zBK>!ya{Nv-{<13jh^4p9&DyVyb8}-!paK>Zz5o@)AbuxFIqFWI8Pb&t#Zvq<8d3F zY#Md3fvpC%KCq2}jSAj66V~i?<{+~qhZdcM?lkazYVcT1CNn2tpKw6w8@Uob@*RAb znK8m$Z~zGoTzp-^i%Kjb5B3$3p!Wd?yqxsEdY1U({MhNFw1wdZan&fR93w_guVt5L z3OmI0id(U(vX-j0Ymr}1+)z~1QdD8LcsUETVV;5`E#2p7cU`78u}hLWz7InQ$INY5 zycgtpkKc?9zd;&GSFZ^Y%r^bOFJ(1urs2))^Ga4Av!X96tsoCUdi=yM`qcnuy7Q`I7JIgAG^4lqG2j>oe}KZVpINBDl`(pId@K| zoeVM@q|X%bLKx$}{=l&V`DaY|w|x{py-+;F&&r4f6+Ly}%B`|MR>M_TrCmjd+dM3d zlUHND)k&QzSX8NtlD{4ue-HYz?ISD{1sd*g@eso6dKp!VrHM>Uzn$$osXxzRj@{Ku zQ_9vY)<`3-z$HIloj~g7|DD`bi4CXB=vw64rJNo6eIQo=(klZpVWa&R7K~8 fX$JssTjm_sY%sp6I#%=;w(}r#Pb8r_IN{Vk?AHOM literal 0 HcmV?d00001 diff --git a/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-firefox-linux.png b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..28117051636c8b5549b1e02d3648a37f5a6da9f7 GIT binary patch literal 16996 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 literal 0 HcmV?d00001 diff --git a/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-firefox-linux.png b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-firefox-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..a9a19b95730d1155e9519973511f9fae2f17058c GIT binary patch literal 30150 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
yi=mYrs QtjkUuwK!5_bng290NA4WH2?qr literal 0 HcmV?d00001 diff --git a/src/sequences/Cached.ts b/src/sequences/Cached.ts index 65d9ffcb..ade24ae2 100644 --- a/src/sequences/Cached.ts +++ b/src/sequences/Cached.ts @@ -393,7 +393,8 @@ export function Cached(desc: PD) { // patchy; all we can rely on is that the closed // interval from the _previous_ value of `this.first` // to `this.lastValueCached` is full. - if (!needsReset + if ( + !needsReset && (this.first < this.firstValueCached || this.first > this.lastValueCached + 1n) ) { @@ -408,7 +409,8 @@ export function Cached(desc: PD) { // And then ditto for factoring. Here we will not // kick off the factoring process because we don't // even know if this sequence is being factored. - if (!needsReset + if ( + !needsReset && (this.first < this.firstFactorCached || this.first > this.lastFactorCached + 1n) ) { diff --git a/src/shared/defineFeatured.ts b/src/shared/defineFeatured.ts index 954ae1c8..6eababfd 100644 --- a/src/shared/defineFeatured.ts +++ b/src/shared/defineFeatured.ts @@ -113,7 +113,7 @@ const featuredSIMs = [ ), specimenQuery( 'Polyfactors', - 'Histogram', + 'FactorHistogram', 'Formula', 'binSize=1', 'formula=n%5E3-n%5E2&length=1000' diff --git a/src/visualizers-workbench/P5VisualizerTemplate.ts b/src/visualizers-workbench/P5VisualizerTemplate.ts index 6f2ad209..f61fa45d 100644 --- a/src/visualizers-workbench/P5VisualizerTemplate.ts +++ b/src/visualizers-workbench/P5VisualizerTemplate.ts @@ -22,7 +22,6 @@ // INVALID_COLOR allows for initializing p5 color variables import {P5Visualizer, INVALID_COLOR} from '../visualizers/P5Visualizer' import {VisualizerExportModule} from '../visualizers/VisualizerInterface' -import type {ViewSize} from '../visualizers/VisualizerInterface' // Standard parameter functionality: import type {GenericParamDescription} from '@/shared/Paramable' diff --git a/src/visualizers/FactorFence.ts b/src/visualizers/FactorFence.ts index a0a1a2af..e270109b 100644 --- a/src/visualizers/FactorFence.ts +++ b/src/visualizers/FactorFence.ts @@ -481,8 +481,9 @@ In addition, several keypress commands are recognized: .fill('red') .text( 'Scanning data to set scale...', - this.size.width/2, - this.size.height/2) + this.size.width / 2, + this.size.height / 2 + ) return } diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index b1a820c6..ea7923e6 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -278,7 +278,8 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.write( `${this.precomputing} ${this.seq.lastFactorCached}`, largeOffsetNumber, - textHeight * 2) + textHeight * 2 + ) this.continue() this.stop(3) } @@ -352,7 +353,8 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Draws the markings on the Y-axis const tickLeft = yAxisPosition - largeOffsetNumber / 5 const tickRight = yAxisPosition + largeOffsetNumber / 5 - const rightJustify = bigTickWidth < tickLeft * scale - 2 * smallOffsetNumber + const rightJustify = + bigTickWidth < tickLeft * scale - 2 * smallOffsetNumber for (let i = 1; i <= nTicks; i++) { // Draws the tick marks let tickY = xAxisHeight - tickHeight * height * i diff --git a/src/visualizers/ModFill.ts b/src/visualizers/ModFill.ts index 6f22a4d1..3eaae9e0 100644 --- a/src/visualizers/ModFill.ts +++ b/src/visualizers/ModFill.ts @@ -1,6 +1,5 @@ import {P5Visualizer, INVALID_COLOR} from './P5Visualizer' import {VisualizerExportModule} from './VisualizerInterface' -import type {ViewSize} from './VisualizerInterface' import {math, MathFormula} from '@/shared/math' import type {GenericParamDescription} from '@/shared/Paramable' diff --git a/src/visualizers/NumberGlyph.ts b/src/visualizers/NumberGlyph.ts index abb43cd7..7ad498b8 100644 --- a/src/visualizers/NumberGlyph.ts +++ b/src/visualizers/NumberGlyph.ts @@ -148,6 +148,7 @@ class NumberGlyph extends P5Visualizer(paramDesc) { // dot control private radii = 50 // increments of radius in a dot private initialRadius = 50 // size of dots + private nGlyphs = 1 // number of glyphs to draw in a frame adjustTermsAndColumns(size: ViewSize) { // Calculate the number of terms we are actually going to show: @@ -220,6 +221,7 @@ class NumberGlyph extends P5Visualizer(paramDesc) { .background('black') .colorMode(this.sketch.HSB, 360, 100, 100) .frameRate(30) + this.nGlyphs = 1 // number of glyphs to draw to keep to 1 per frame } draw() { @@ -227,17 +229,25 @@ class NumberGlyph extends P5Visualizer(paramDesc) { this.sketch .fill('red') .text( - 'Factoring...', this.size.width / 2, this.size.height / 2) + 'Factoring...', + this.size.width / 2, + this.size.height / 2 + ) + ++this.nGlyphs // make sure we make up for lost time return } - if (this.changePosition()) this.sketch.background('black') - this.sketch.noStroke() - if (this.currentIndex > this.last) { - this.stop() - return + for (let i = 0; i < this.nGlyphs; ++i) { + if (this.changePosition()) this.sketch.background('black') + this.sketch.noStroke() + if (this.currentIndex > this.last) { + this.stop() + this.nGlyphs = 1 + return + } + this.drawCircle(this.currentIndex) + ++this.currentIndex } - this.drawCircle(this.currentIndex) - ++this.currentIndex + this.nGlyphs = 1 } drawCircle(ind: bigint) { @@ -302,26 +312,21 @@ class NumberGlyph extends P5Visualizer(paramDesc) { changePosition(): boolean { this.initialRadius = Math.floor(this.positionIncrement / 2) this.radii = this.initialRadius - let first = false if (this.position.x === 0) { - first = true this.initialPosition = this.sketch.createVector( this.initialRadius, this.initialRadius ) this.position = this.initialPosition.copy() - } else this.position.add(this.positionIncrement, 0) + return true + } + this.position.add(this.positionIncrement, 0) // if we need to go to next line - if ( - math.divides( - this.columns, - this.currentIndex - this.seq.first + 1n - ) - ) { + if (math.divides(this.columns, this.currentIndex - this.seq.first)) { this.position.x = this.initialPosition.x this.position.add(0, this.positionIncrement) } - return first + return false } isPrime(ind: bigint): boolean { diff --git a/src/visualizers/P5Visualizer.ts b/src/visualizers/P5Visualizer.ts index 94d1c5cc..c9eaf911 100644 --- a/src/visualizers/P5Visualizer.ts +++ b/src/visualizers/P5Visualizer.ts @@ -197,7 +197,7 @@ export function P5Visualizer(desc: PD) { if (!where) return true if ( where !== this.within - && !where.contains(this.within) + && !where.contains(this.within) ) { return true } @@ -206,7 +206,10 @@ export function P5Visualizer(desc: PD) { } continue } - if (method === 'mouseReleased' || method === 'mouseMoved') { + if ( + method === 'mouseReleased' + || method === 'mouseMoved' + ) { if (trivial) { sketch[method] = (event: MouseEvent) => { this.mousePrimaryDown = isPrimaryDown(event) @@ -223,15 +226,15 @@ export function P5Visualizer(desc: PD) { if (trivial) continue if ( method === 'keyPressed' - || method === 'keyReleased' - || method === 'keyTyped' + || method === 'keyReleased' + || method === 'keyTyped' ) { sketch[method] = (event: KeyboardEvent) => { const active = document.activeElement if ( active - && (active.tagName === 'INPUT' - || active.tagName === 'TEXTAREA') + && (active.tagName === 'INPUT' + || active.tagName === 'TEXTAREA') ) { return true } From 121234bdd86c9b90c345eb9e24f274ff1719143e Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 26 Nov 2025 15:28:50 -0800 Subject: [PATCH 08/12] fix: show finer-grained progress on factoring long sequence --- src/sequences/Cached.ts | 4 ++-- src/sequences/SequenceInterface.ts | 5 +++-- src/visualizers/FactorHistogram.ts | 7 ++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/sequences/Cached.ts b/src/sequences/Cached.ts index ade24ae2..98afd820 100644 --- a/src/sequences/Cached.ts +++ b/src/sequences/Cached.ts @@ -257,7 +257,7 @@ export function Cached(desc: PD) { async fillValueCache(n: bigint) { const start = this.lastValueCached + 1n for (let i = start; i <= n; i++) { - if (i % 10000n == 0n) await yieldExecution() + if (i % 10000n === 0n) await yieldExecution() const key = i.toString() // trust values we find; hopefully we have cleared when // needed, so that we can presume they are from some @@ -299,7 +299,7 @@ export function Cached(desc: PD) { const start = this.lastFactorCached + 1n for (let i = start; i <= n; ++i) { // Can we yield execution? - if (i % 10000n == 0n) await yieldExecution() + if (i % 10000n === 0n) await yieldExecution() const key = i.toString() // trust values we find if (!(key in this.factorCache)) { diff --git a/src/sequences/SequenceInterface.ts b/src/sequences/SequenceInterface.ts index 333ea092..e83fe1c6 100644 --- a/src/sequences/SequenceInterface.ts +++ b/src/sequences/SequenceInterface.ts @@ -33,11 +33,12 @@ export interface SequenceInterface extends ParamableInterface { readonly length: ExtendedBigint /** - * HACK: We make the last factor cached accessible for the sake of - * showing progress information on long caching runs. + * HACKS: We make the last value cached and last factor cached accessible + * for the sake of showing progress information on long caching runs. * TODO: add a 'progress' function or something like that to do such * things in a more disciplined and flexible way. */ + readonly lastValueCached: bigint readonly lastFactorCached: bigint /** diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index ea7923e6..51785d94 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -128,6 +128,8 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.precomputing = '' return } + this.precomputing = 'Calculating sequence values ...' + await this.seq.fill(this.endIndex()) this.precomputing = 'Factoring ...' await this.seq.fill(this.endIndex(), 'factors') this.precomputing = 'Collecting values ...' @@ -275,8 +277,11 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { if (this.precomputing) { sketch.fill('red') + const position = this.precomputing.startsWith('Factor') + ? this.seq.lastFactorCached + : this.seq.lastValueCached this.write( - `${this.precomputing} ${this.seq.lastFactorCached}`, + `${this.precomputing} ${position}`, largeOffsetNumber, textHeight * 2 ) From 54f23e87fddc8adf3868cc53f77e3d0ffb5df077 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Wed, 26 Nov 2025 23:39:26 -0800 Subject: [PATCH 09/12] fix: make sure the factor counts are recomputed when parameter change --- src/visualizers/FactorHistogram.ts | 39 ++++++++++++++++++------------ 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index 51785d94..1cf1175b 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -68,6 +68,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { 'Displays a histogram of the number of prime factors of a sequence' precomputing = '' + factorCount: number[] = [] binFactorArray: number[] = [] numUnknown = 0 fontsLoaded = false @@ -94,8 +95,8 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Create an array with the value at n being the number of entries // of the sequence having n factors. Entries with unknown factorization // are put into -1 - async factorCounts(): Promise { - const factorCount = [] + async factorCounts() { + this.factorCount = [] const last = this.endIndex() for (let i = this.seq.first; i <= last; i++) { if (i % 10000n === 0n) await yieldExecution() @@ -113,10 +114,9 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { if (counter === 0 && math.bigabs(this.seq.getElement(i)) > 1) { counter = -1 } - if (counter in factorCount) factorCount[counter]++ - else factorCount[counter] = 1 + if (counter in this.factorCount) this.factorCount[counter]++ + else this.factorCount[counter] = 1 } - return factorCount } // Create an array with the frequency of each number @@ -133,17 +133,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.precomputing = 'Factoring ...' await this.seq.fill(this.endIndex(), 'factors') this.precomputing = 'Collecting values ...' - const factorCount = await this.factorCounts() - let largestValue = factorCount.length - 1 - if (largestValue < 0) largestValue = 0 - this.binFactorArray = new Array(this.binOf(largestValue) + 1).fill(0) - factorCount.forEach( - (count, ix) => (this.binFactorArray[this.binOf(ix)] += count) - ) - if ((-1) in factorCount) { - this.numUnknown = factorCount[-1] - this.binFactorArray[0] += this.numUnknown - } else this.numUnknown = 0 + await this.factorCounts() this.precomputing = '' } @@ -190,6 +180,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.sketch.textFont(font) this.fontsLoaded = true }) + this.binFactorArray = [] } barLabel(binIndex: number) { @@ -287,6 +278,22 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { ) this.continue() this.stop(3) + return + } + + if (this.binFactorArray.length === 0) { + let largestValue = this.factorCount.length - 1 + if (largestValue < 0) largestValue = 0 + this.binFactorArray = new Array( + this.binOf(largestValue) + 1 + ).fill(0) + this.factorCount.forEach( + (count, ix) => (this.binFactorArray[this.binOf(ix)] += count) + ) + if ((-1) in this.factorCount) { + this.numUnknown = this.factorCount[-1] + this.binFactorArray[0] += this.numUnknown + } else this.numUnknown = 0 } const binWidth = this.binWidth() From 87709e8e0cbcb737e5307430115cd36c8a6e9eaa Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 28 Nov 2025 10:43:23 -0800 Subject: [PATCH 10/12] chore: reword hover box caption --- src/visualizers/FactorHistogram.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index 1cf1175b..a080b412 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -201,30 +201,27 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Literally no idea why we only have to scale when scale > 1 :-/ // but there's no arguing with it looking right if (scale > 1) textVerticalSpacing *= scale - let boxHeight = textVerticalSpacing * 2.4 - if (showUnknown) boxHeight += textVerticalSpacing const margin = offset const boxRadius = Math.floor(margin) // Set up the texts to display: - const captions = ['Factors: ', 'Height: '] - const values = [ - this.barLabel(binIndex), - this.binFactorArray[binIndex].toString(), + const amt = this.binFactorArray[binIndex] + const cnt = this.barLabel(binIndex) + const captions = [ + `${amt} number${amt !== 1 ? 's' : ''} with`, + `${cnt} prime factor${cnt !== '1' ? 's' : ''}`, ] if (showUnknown) { - captions.push('Unknown: ') - values.push(this.numUnknown.toString()) + captions.push(`(${this.numUnknown} with`) + captions.push(`unknown factors)`) } - let captionWidth = 0 let totalWidth = 0 for (let i = 0; i < captions.length; ++i) { - let width = sketch.textWidth(captions[i]) - if (width > captionWidth) captionWidth = width - width += sketch.textWidth(values[i]) + const width = sketch.textWidth(captions[i]) if (width > totalWidth) totalWidth = width } totalWidth += 2 * margin + const boxHeight = textVerticalSpacing * (captions.length + 0.5) // don't want box to wander past right edge of canvas const boxX = Math.min(pX, sketch.width - totalWidth) @@ -242,7 +239,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { for (let i = 0; i < captions.length; ++i) { this.write( - captions[i] + values[i], + captions[i], boxX + margin, boxY + (i + 1) * textVerticalSpacing ) From 50837fafa83dc735126b15feed3850a060eb0f6e Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 28 Nov 2025 17:11:55 -0800 Subject: [PATCH 11/12] chore: remaining PR feedback, remove now-detrimental hover scaling --- e2e/tests/featured.spec.ts | 6 ++---- src/visualizers/FactorHistogram.ts | 17 +++++++---------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/e2e/tests/featured.spec.ts b/e2e/tests/featured.spec.ts index d8ccdbf9..02a74184 100644 --- a/e2e/tests/featured.spec.ts +++ b/e2e/tests/featured.spec.ts @@ -15,7 +15,7 @@ test.describe('Featured gallery images', () => { ) { details.tag = '@webGL' } - test(featProps.name, details, async ({page, browserName}) => { + test(featProps.name, details, async ({page}) => { const short = encodeURIComponent( featProps.name.replaceAll(' ', '') ) @@ -27,9 +27,7 @@ test.describe('Featured gallery images', () => { timeout: featProps.visualizerKind === 'Chaos' ? 60000 : 30000, }) const matchParams = - browserName === 'firefox' && details.tag === '@webGL' - ? {maxDiffPixelRatio: 0.01} - : {} + details.tag === '@webGL' ? {maxDiffPixelRatio: 0.01} : {} expect( await page.locator('#canvas-container').screenshot() ).toMatchSnapshot(`${short}.png`, matchParams) diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index a080b412..29414f30 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -29,7 +29,7 @@ The height of each bar shows how many entries in the sequence have a corresponding value of Omega. The leftmost bar corresponding to 0 factors counts all sequence entries equal to 0 or 1, as well as those entries that could not be factored (because their values were too large for Numberscope -to handle). The latter are indicated by hashing a portion of that leftmost bar +to handle). The latter are indicated by hatching a portion of that leftmost bar proportional to the number of unfactored entries. ## Parameters @@ -195,12 +195,9 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { drawHoverBox(binIndex: number, offset: number) { const sketch = this.sketch - const {pX, pY, scale} = this.mouseToPlot() + const {pX, pY} = this.mouseToPlot() const showUnknown = binIndex === 0 && this.numUnknown > 0 - let textVerticalSpacing = sketch.textAscent() + 1 - // Literally no idea why we only have to scale when scale > 1 :-/ - // but there's no arguing with it looking right - if (scale > 1) textVerticalSpacing *= scale + const textVerticalSpacing = sketch.textAscent() + 1 const margin = offset const boxRadius = Math.floor(margin) @@ -229,7 +226,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // create the box itself sketch.push() - sketch.translate(0, 0, 2) + sketch.translate(0, 0, 0.5) sketch.fill('white') sketch.rect(boxX, boxY, totalWidth, boxHeight, boxRadius) @@ -312,7 +309,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { // Draw the x-axis sketch.line(0, xAxisHeight, sketch.width, xAxisHeight) - for (let i = 0; i < 30; i++) { + for (let i = 0; i < this.binFactorArray.length; i++) { if (this.mouseOver && inBin && i == binIndex) { sketch.fill(200, 200, 200) } else { @@ -404,11 +401,11 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { 0.1 * sketch.height ) this.write( - `Too many bins (${this.binFactorArray.length}),`, + `With ${this.binFactorArray.length} bins, zoom\n` + + 'or pan to see more', pX, pY - textHeight * 3 ) - this.write('Displaying the first 30.', pX, pY - textHeight * 1.3) } // If mouse interaction, draw hover box if (this.mouseOver && inBin) { From 43ab6294fc20ab3c9c0c59b3fdb756d734378a37 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 28 Nov 2025 19:11:48 -0800 Subject: [PATCH 12/12] fix: don't draw a y-axis tick mark that will get cut off; revert ci.yaml --- .github/workflows/ci.yaml | 12 +++++------ src/visualizers/FactorHistogram.ts | 32 +++++++++++++++--------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 58fede86..e79c6bc5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -49,9 +49,9 @@ jobs: - name: End-to-end test run: npx playwright test -c e2e/playci.config.ts ### Uncomment when you need to re-extract snapshots - - name: Extract snapshots - if: always() - uses: actions/upload-artifact@v4 - with: - name: ci_actual_snapshots - path: e2e/results/manual/output +# - name: Extract snapshots +# if: always() +# uses: actions/upload-artifact@v4 +# with: +# name: ci_actual_snapshots +# path: e2e/results/manual/output diff --git a/src/visualizers/FactorHistogram.ts b/src/visualizers/FactorHistogram.ts index 29414f30..f0b15d5e 100644 --- a/src/visualizers/FactorHistogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -99,7 +99,10 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.factorCount = [] const last = this.endIndex() for (let i = this.seq.first; i <= last; i++) { - if (i % 10000n === 0n) await yieldExecution() + if (i % 10000n === 0n) { + this.precomputing = `Collecting values ... ${i} /` + await yieldExecution() + } let counter = 0 const factors = this.seq.getFactors(i) if (factors) { @@ -356,27 +359,24 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { nTicks = Math.floor(sketch.height / (height * tickHeight)) const bigTick = nTicks * tickHeight const bigTickWidth = sketch.textWidth(bigTick.toString()) - // Draws the markings on the Y-axis + // Draw the markings on the Y-axis const tickLeft = yAxisPosition - largeOffsetNumber / 5 const tickRight = yAxisPosition + largeOffsetNumber / 5 const rightJustify = bigTickWidth < tickLeft * scale - 2 * smallOffsetNumber for (let i = 1; i <= nTicks; i++) { - // Draws the tick marks - let tickY = xAxisHeight - tickHeight * height * i - sketch.line(tickLeft, tickY, tickRight, tickY) - - const label = (tickHeight * i).toString() - let tickPos = tickRight + smallOffsetNumber - if (rightJustify) { - const labelWidth = sketch.textWidth(label) - tickPos = tickLeft - labelWidth - smallOffsetNumber - } - + const tickY = xAxisHeight - tickHeight * height * i + const textY = tickY + textHeight / 2.5 // Avoid placing text that will get cut off - tickY += textHeight / 2.5 - if (tickY > sketch.textAscent()) { - this.write(label, tickPos, tickY) + if (textY > sketch.textAscent()) { + sketch.line(tickLeft, tickY, tickRight, tickY) + const label = (tickHeight * i).toString() + let tickPos = tickRight + smallOffsetNumber + if (rightJustify) { + const labelWidth = sketch.textWidth(label) + tickPos = tickLeft - labelWidth - smallOffsetNumber + } + this.write(label, tickPos, textY) } }