diff --git a/doc/visualizer-in-depth.md b/doc/visualizer-in-depth.md index b8a9ef27..697d4feb 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 @@ -390,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 @@ -409,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/e2e/tests/featured.spec.ts b/e2e/tests/featured.spec.ts index 7bce2a62..02a74184 100644 --- a/e2e/tests/featured.spec.ts +++ b/e2e/tests/featured.spec.ts @@ -9,13 +9,13 @@ 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' ) { 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/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 00000000..64c31df1 Binary files /dev/null and b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-chromium-linux.png differ 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 00000000..28117051 Binary files /dev/null and b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA001220-firefox-linux.png differ diff --git a/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-chromium-linux.png b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-chromium-linux.png new file mode 100644 index 00000000..b1213926 Binary files /dev/null and b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-chromium-linux.png differ 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 00000000..a9a19b95 Binary files /dev/null and b/e2e/tests/transversal.spec.ts-snapshots/FactorHistogramA002819-firefox-linux.png differ 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..98afd820 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)) { @@ -393,10 +394,13 @@ export function Cached(desc: PD) { // interval from the _previous_ value of `this.first` // to `this.lastValueCached` is full. if ( - this.first < this.firstValueCached - || this.first > this.lastValueCached + 1n + !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: @@ -406,8 +410,9 @@ export function Cached(desc: PD) { // 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 + !needsReset + && (this.first < this.firstFactorCached + || this.first > this.lastFactorCached + 1n) ) { await this.factorCachingPromise this.firstFactorCached = math.posInfinity @@ -441,7 +446,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..e83fe1c6 100644 --- a/src/sequences/SequenceInterface.ts +++ b/src/sequences/SequenceInterface.ts @@ -32,6 +32,15 @@ export interface SequenceInterface extends ParamableInterface { */ readonly length: ExtendedBigint + /** + * 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 + /** * 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/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/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..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' @@ -113,14 +112,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..e270109b 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,18 @@ 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/Histogram.ts b/src/visualizers/FactorHistogram.ts similarity index 70% rename from src/visualizers/Histogram.ts rename to src/visualizers/FactorHistogram.ts index 94bb796d..f0b15d5e 100644 --- a/src/visualizers/Histogram.ts +++ b/src/visualizers/FactorHistogram.ts @@ -2,8 +2,9 @@ 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} from '@/shared/Paramable' +import type {GenericParamDescription, ParamValues} from '@/shared/Paramable' import {ParamType} from '@/shared/ParamType' import {ValidationStatus} from '@/shared/ValidationStatus' @@ -25,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 hatching a portion of that leftmost bar +proportional to the number of unfactored entries. ## Parameters **/ @@ -63,19 +67,26 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { static description = 'Displays a histogram of the number of prime factors of a sequence' - factoring = true + precomputing = '' + factorCount: number[] = [] binFactorArray: number[] = [] 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 @@ -84,9 +95,14 @@ 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[] { - const factorCount = [] - for (let i = this.seq.first; i <= this.endIndex(); i++) { + async factorCounts() { + this.factorCount = [] + const last = this.endIndex() + for (let i = this.seq.first; i <= last; i++) { + if (i % 10000n === 0n) { + this.precomputing = `Collecting values ... ${i} /` + await yieldExecution() + } let counter = 0 const factors = this.seq.getFactors(i) if (factors) { @@ -101,28 +117,27 @@ 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 // of factors in the corresponding bins - async binFactorArraySetup() { + async presketch(seqChanged: boolean, sizeChanged: boolean) { + this.precomputing = 'Evaluating sequence entries ...' + await super.presketch(seqChanged, sizeChanged) + if (!seqChanged) { + this.precomputing = '' + return + } + this.precomputing = 'Calculating sequence values ...' + await this.seq.fill(this.endIndex()) + this.precomputing = 'Factoring ...' await this.seq.fill(this.endIndex(), 'factors') - const factorCount = 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 - this.factoring = false + this.precomputing = 'Collecting values ...' + await this.factorCounts() + this.precomputing = '' } // Create a number that represents how @@ -168,8 +183,7 @@ class FactorHistogram extends P5GLVisualizer(paramDesc) { this.sketch.textFont(font) this.fontsLoaded = true }) - this.factoring = true - this.binFactorArraySetup() + this.binFactorArray = [] } barLabel(binIndex: number) { @@ -184,36 +198,30 @@ 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 - let boxHeight = textVerticalSpacing * 2.4 - if (showUnknown) boxHeight += textVerticalSpacing + const textVerticalSpacing = sketch.textAscent() + 1 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) @@ -221,7 +229,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) @@ -231,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 ) @@ -249,17 +257,40 @@ 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 const smallOffsetNumber = (1 - smallOffsetScalar) * sketch.width - if (this.factoring) { + if (this.precomputing) { sketch.fill('red') - this.write('Factoring ...', largeOffsetNumber, textHeight * 2) + const position = this.precomputing.startsWith('Factor') + ? this.seq.lastFactorCached + : this.seq.lastValueCached + this.write( + `${this.precomputing} ${position}`, + largeOffsetNumber, + textHeight * 2 + ) 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() @@ -267,8 +298,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) } @@ -279,7 +312,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 { @@ -326,26 +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 - 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 - 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) } } @@ -370,25 +401,25 @@ 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) { 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.precomputing || 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/ModFill.ts b/src/visualizers/ModFill.ts index eb298651..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' @@ -202,16 +201,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..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: @@ -169,13 +170,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,34 +214,40 @@ 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() + this.nGlyphs = 1 // number of glyphs to draw to keep to 1 per frame } draw() { - this.sketch.noStroke() - if (this.currentIndex > this.last) { - this.stop() + if (!this.presketchComplete) { + this.sketch + .fill('red') + .text( + 'Factoring...', + this.size.width / 2, + this.size.height / 2 + ) + ++this.nGlyphs // make sure we make up for lost time return } - this.drawCircle(this.currentIndex) - this.changePosition() - ++this.currentIndex + 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.nGlyphs = 1 } drawCircle(ind: bigint) { @@ -301,18 +308,25 @@ class NumberGlyph extends P5Visualizer(paramDesc) { } } - changePosition() { + // returns true on first draw, false otherwise + changePosition(): boolean { + this.initialRadius = Math.floor(this.positionIncrement / 2) + this.radii = this.initialRadius + if (this.position.x === 0) { + this.initialPosition = this.sketch.createVector( + this.initialRadius, + this.initialRadius + ) + this.position = this.initialPosition.copy() + 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 false } isPrime(ind: bigint): boolean { diff --git a/src/visualizers/P5Visualizer.ts b/src/visualizers/P5Visualizer.ts index ad84f73a..c9eaf911 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,17 +71,25 @@ 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 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 } @@ -97,7 +105,9 @@ export function P5Visualizer(desc: PD) { _canvas?: p5.Renderer _framesRemaining = Infinity size = nullSize + mousePrimaryDown = false drawingState: DrawingState = DrawingUnmounted + presketchComplete = false within?: HTMLElement popup?: HTMLElement = undefined @@ -160,60 +170,80 @@ 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 - && !where.contains(this.within) - ) { - return true - } - return this[method](event as never) - // Cast makes typescript happy :-/ + this.mousePrimaryDown = isPrimaryDown(event) + return true } continue } - if ( - method === 'keyPressed' - || method === 'keyReleased' - || method === 'keyTyped' - ) { - sketch[method] = (event: KeyboardEvent) => { - const active = document.activeElement - if ( - active - && (active.tagName === 'INPUT' - || active.tagName === 'TEXTAREA') - ) { - return true - } - return this[method](event) + 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 :-/ + } + continue + } + if ( + method === 'mouseReleased' + || method === 'mouseMoved' + ) { + if (trivial) { + sketch[method] = (event: MouseEvent) => { + this.mousePrimaryDown = isPrimaryDown(event) + return true } continue } - // Otherwise no special condition, just forward event - sketch[method] = definition.bind(this) as () => void + 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 + && (active.tagName === 'INPUT' + || active.tagName === 'TEXTAREA') + ) { + return true + } + return this[method](event) + } + continue } + // Otherwise no special condition, just forward event + sketch[method] = definition.bind(this) as () => void } // And draw is special because of the error handling: @@ -259,25 +289,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) } @@ -287,8 +319,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() } /**