CreepJS Canvas Seed Cycle: How Many Rounds Stay Distinct
CreepJS multiplies a canvas seed by 15000 modulo 2001000001 every round. This probe measures how many rounds stay distinct before that sequence repeats.
BotBrowser Team
Want the structured docs for Fingerprint?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
BotBrowser can replay a declared profile against a page-owned canvas fixture and keep a bounded record of what the page drew; it does not make a short drawing represent a long one, and it cannot change how a library generates its own seed. The CreepJS canvas module builds each round from a state that is multiplied by a constant and reduced modulo another constant, and how long that sequence stays distinct before it repeats decides how much of a long drawing is new. The CreepJS emoji metrics analysis separates representative text geometry from this drawing sequence and from pixel output.
TL;DR
- CreepJS initialises its canvas seed at 500, multiplies by 15000, and reduces modulo 2001000001, then draws 10 rounds by default.
- Those published defaults produce 8 distinct values in the first 8 draws, and 10 distinct integer positions across 10 consecutive generator states.
- The multiplicative sequence repeats after exactly 26940 draws, so the default 10-round drawing never revisits a state.
- Five rounds of that drawing are a prefix of ten: a shorter page and a longer page share their first five values.
- Changing only the seed moves the sequence but leaves the length unchanged for the four starts tested: 500, 501, 1000 and 2000 all repeat at 26940. A start sharing a factor with the modulus would enter a shorter cycle.
- A report that records "a canvas digest" without round count, seed and source revision cannot say how much of the drawing was new.
Contents
- The published generator
- How many rounds stay distinct
- What a prefix means for comparison
- Where the count stops being informative
- Record a canvas reading
The published generator
The module defines one small generator and one mapping from its raw value to a canvas coordinate: The module is a canvas fingerprint, so its first job is to draw something whose readback differs across machines while staying identical for one machine. The generator supplies the deterministic half of that: given a seed it always walks the same states, and the mapping turns each state into a coordinate. The rendering half, where a GPU or a software rasterizer turns those coordinates into pixels, is what the library cannot make deterministic from the seed alone.
source -> https://raw.githubusercontent.com/abrahamjuliot/creepjs/master/src/canvas/index.ts
source_bytes -> 22274 sha256=6bb5e580122bb26343e1fd8108fb2db6e5a719ab8beebf04394779f06139047d
defaults -> seed=500 offset=2001000001 multiplier=15000
rounds_default -> 10
The same distinction separates the readback digest of the canvas element from the coordinates the library asked it to paint. The digest collapses everything the renderer did into one value; the coordinate sequence shows what the library requested before the renderer touched it. Keeping both is what lets a reviewer say whether two runs differ in the request or in the painting. The generator keeps a current value below the modulus. Each call multiplies it by 15000 and takes the remainder, and the mapping divides by the modulus, scales by the canvas dimension and floors the result. Both steps are integer arithmetic with no randomness at draw time, which is why the same seed on the same source revision produces the same round positions.
That is a deliberate design. A canvas fingerprint wants to be stable for a given machine and library version, so it cannot draw fresh random pixels on every call. It starts from a seed and derives everything after it, and the seed is the part a machine or a profile supplies.
How many rounds stay distinct
With those defaults the first draws are all different:
first_draws -> 7500000 443999944 671156672 319074969 1733532609 1995122006 1875075045 69660944
first_draws_distinct -> 8 of 8
round_positions -> distinct=10 of 10 rounds
The eight values below are the raw output of the generator, before the modulo result is divided by the modulus and scaled to a pixel. They are the cleanest evidence in the run because they have no drawing size baked in: two reports with different canvas dimensions can still be compared on these eight numbers, while their pixel positions may already have diverged.
Eight raw values and ten integer positions, no repeats across ten consecutive generator states. That is a ten-state window, not a count of the states one round consumes: the module calls getNextSeed() several times per round, for the gradient, the shadow and the drawing method, so one round advances more than one state.
The cycle length is much larger than the default:
cycle -> first_repeat_index=0 length=26940
cycle_covers_rounds -> true
26940 draws before the sequence returns to its starting value. The default 10 rounds use 10 of those 26940 states, so the drawing is a tiny prefix of a much longer sequence. A page that asked for 500 rounds would still never repeat a state, and a page that asked for 27000 would reach the cycle's edge.
What a prefix means for comparison
A page can also keep a drawing alive and append to it. In that shape the first five states of the second drawing are not the first five states of the sequence at all, because the generator is already further along; the prefix property only holds when the page re-seeds. The same reset question for a profile is what noise seed reproducibility records.
A shorter drawing and a longer drawing with the same parameters do not differ from the start; one contains the other. The same difference between a stable seed and a rendered surface is what canvas fingerprinting records on the surface side.
prefix -> five_rounds_are_prefix_of_ten=true
A canvas.toDataURL() digest therefore hides whether the tenth round was added: the first rounds match either way.
The same check covers the round count. A report that names five rounds and one that names ten share the same source and seed, so their first five values agree; only the digest differs, and only the round count explains why. A reviewer who compares digests without the round count will treat a page edit as an environment change. The multiplier of 15000 and the modulus of 2001000001 are the two numbers to keep, so that structure explains why two CreepJS reports from different pages can differ while describing the same machine. The generator's state is deterministic, so the only variables that move are the seed, the round count and the source revision. Two pages that share all three produce identical round positions, and two pages that share only the seed diverge from the first extra round. A comparison that does not know the round count has no way to decide which of those cases it is looking at.
A reviewer comparing two runs therefore needs to keep the canvas dimensions beside the seed and the round count. Without them a difference in position can point at a different seed, a different round count, a different canvas size or a different source revision, and a report that keeps only the rendered result cannot separate the four.
Recording seed=500 and rounds=10 makes that check possible. If the report contains the seed, the round count and the source revision, the sequence can be recomputed offline with the published constants and compared with the positions a page recorded; a mismatch points at the page, and a match leaves the renderer as the next place to look. A report without those three fields forces the reviewer to re-run the page, which changes the evidence instead of checking it.
The mapping has one more property worth recording: it floors rather than rounds. A state that maps to 37.9 lands on pixel 37, and a state that maps to 38.1 lands on pixel 38. Two states within a narrow band can share a coordinate at one drawing size and split at another. A reviewer who compares pixel coordinates across two drawing sizes is comparing a floored projection of the sequence, not the sequence itself.
reusing the seed restarts at the same place and produces identical rounds, while reusing the generator continues from wherever the last drawing stopped. A report with a draw index can say whether it captured the first drawing or a later one, and a report without it cannot tell the two strategies apart.
A library that already ships a deterministic seed still has to decide when to reset it. Reusing the seed restarts at the same place and reproduces identical rounds, while keeping one generator alive continues from the last drawing and produces a different one. Both choices are defensible for a library; only the record has to say which one produced the report. The renderer step also deserves its own reading. A software rasterizer and a hardware rasterizer can disagree on edge pixels even when both receive the same rectangle. The seed sequence tells a reviewer which rectangle was requested, and the renderer tells them which graphics path answered. Only when both match does a matching digest become evidence that two environments rendered identically.
Where the count stops being informative
The multiplicative step is not the only part of the module worth reading. The generator is seeded once per drawing, then advanced for the gradient, the shadow blur, the shadow colour and the drawing method, so a single round consumes several states and the state index moves faster than the round index. The published colour table holds fifty entries and each round picks two stops, so once the state index passes fifty picks the table repeats while the state itself does not: a repeated colour is not evidence of a repeated round.
One addressable figure comes from the modulus and the multiplier rather than from the seed:
The value multiplier=15000 and the modulus offset=2001000001 fix it, so a report can cite both.
seed:500 -> cycle=26940 draws_8_distinct=1
seed:501 -> cycle=26940 draws_8_distinct=1
seed:1000 -> cycle=26940 draws_8_distinct=1
seed:2000 -> cycle=26940 draws_8_distinct=1
The cycle length itself is a property of the modular arithmetic, not of the seed. With a fixed modulus and multiplier the walk visits a cycle whose length depends only on the step; every starting point that is coprime with the modulus lands on that same cycle and every starting point that shares a factor lands on a shorter one. That is why the probe reports the cycle next to the seed instead of in place of it. The second column of that output is misleading on purpose, and it shows a measurement trap. The probe recreated the generator inside a single expression for each of the eight draws, so every call started from the seed again and returned the same first value. A probe written that way reports one distinct value and concludes that the generator is degenerate. The correct reading is the one measured earlier: eight distinct values from one generator instance.
That trap is worth stating because it is the most common way a seed probe produces a false positive: creating the generator inside the loop, or resetting it between draws, measures the first value repeatedly and never touches the sequence. The realistic limit is different. A page that draws more than 26940 rounds would see a repeated state, while the default 10 stays far from that boundary, so the cycle length tells a reviewer how far a drawing can grow before it wraps.
The distinction appears in a report as a count. One generator instance produces one sequence; two drawings from one generator produce the first and second segment of that sequence, which is the only shape that differs from a clean run. The two other published constants belong to the same explanation: in this probe gcd(15000, 2001000001) is 1, so the multiplier and modulus keep a full-length cycle for the four coprime starts tested, while a multiplier sharing a factor with the modulus would collapse the sequence into a shorter cycle.
Two machines running the same seed=500 drawing could still render different pixels if one has a different graphics path. The sequence from multiplier=15000 decides which rectangle receives which color, and the graphics path decides what color the renderer actually paints. A report that stores only the sequence misses the rendering half, and a report that stores only the rendered pixels misses the sequence half. A useful record contains both.
Record a canvas reading
The report is about the request, so its fields mirror the request rather than the picture. A picture can look the same after a renderer change and a request can look the same after a scale change, and only a record that separates request fields from picture fields can tell those two cases apart.
A canvas reading supports a narrow claim: this source revision, this seed, this many rounds, produced these distinct positions. Four fields keep it checkable, and rounds and seed are two of them:
The minimum record keeps seed, rounds, the source digest and the canvas size.
The renderer name belongs in the same record for a reason that only shows up on a second machine. Two hosts can run the identical sequence and still paint different pixels, because one uses a software rasterizer and the other a hardware path. The sequence explains which rectangle each state requested; the renderer explains what that rectangle came out as. A record that keeps the sequence but drops the renderer cannot tell a genuine drawing change from a different graphics path.
The same record should keep the drawing size in pixels rather than a fraction of the screen. A 50-pixel canvas and a 100-pixel canvas floor the same state to different coordinates, and a report normalized to a percentage cannot be recomputed offline. Absolute pixels keep the mapping reproducible from the published constants.
Two timestamps are also useful when a report is compared months later. ranAt says when the probe read the source, and the recorded source digest says which revision it actually read, because the published file can change between runs. A report with both can be reproduced; a report with a date but no digest cannot say which version of the arithmetic it used.
- The source revision and digest, because a change to the multiplier or the modulus changes every draw.
- The seed and the round count, because they decide which part of the sequence a drawing uses.
- The distinct-state and distinct-position counts, because a digest alone cannot say whether a round was added or the seed moved.
- The generator lifetime, because recreating the generator per draw turns the sequence into a repeated first value.
That division also keeps a canvas reading from overstating what it measured. A matching sequence says the page used the same seed and the same round count. A matching renderer says the graphics path reported the same name. A matching digest says the final pixels matched. Each is a separate claim, and a report that collapses them into "canvas matched" has three claims behind one word.
The font table article reads a third public table set the same way, and its counts and buckets sit beside the canvas seed cycle.
Public sources
- CreepJS
- CreepJS canvas module
- MDN: CanvasRenderingContext2D
- BotBrowser: automation consistency reference
Practical conclusion
Record the seed, the round count and the source revision with every canvas comparison, because a digest alone cannot tell a longer drawing from a different one. Treat five rounds as a prefix of ten when the seed and source revision match, and expect the digest to differ anyway. Read the 26940-draw cycle as the distance a drawing can grow before it repeats, not as a property of the default ten. For the arithmetic behind a longer canvas comparison, read how CreepJS groups its canvas signals; for where a page-side reading stops and a connection-layer one begins, read edge-layer rules a page script cannot evaluate.
This article is part of the guide: What Is a Browser Fingerprint?
Related Articles
Deterministic Mode: Reproducible Browser Fingerprints
FingerprintNoise Seed Reproducibility for Repeatable Fingerprint Runs
FingerprintCreepJS Emoji Metrics: What Sets and Sums Reveal
FingerprintCanvas Fingerprinting: How It Works and What Helps
FingerprintText Measurement Fingerprinting: Sub-Pixel Tracking
FingerprintCreepJS Font Tables: 51 Names and 14 Possible Returns
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.