Skip to content

Fix/348 modify mix value calculations #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

DarioSoller
Copy link
Contributor

Fixes #348 and might enhance possible calculation resolution generically for all modifier properties, as suggested in the issue discussion

Copy link

changeset-bot bot commented Mar 20, 2025

⚠️ No Changeset found

Latest commit: bbc7400

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@DarioSoller DarioSoller force-pushed the fix/348-modify-mix-value-calculations branch from 8a42631 to 0e7ce70 Compare March 24, 2025 00:17
@DarioSoller
Copy link
Contributor Author

DarioSoller commented Mar 24, 2025

@jorenbroekema I came up with a fix and also added a unit and some integration tests. Please check if you have concerns and if I have been on the right direction.

But I have to say that I had some issues with running the test on my machine. Either the unit or the integration tests failed. I suspected being on the wrong nodeJS version, but there is no .nvmrc file and I have only seen the node versions used in the Github action runner. I therefore tried node version v18.16.0 and v20.10.0. Also because I am using NVM, the Husky precommit hook failed, without me adding a NVM environment variable.

Another possibility could be, that the package-lock.json is outdated? Here the issues I have been running into:

Running the unit tests without updating the package-lock.json (integration tests run here!)

Error: Cannot find module @rollup/rollup-darwin-arm64. npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828).
Please try `npm i` again after removing both package-lock.json and node_modules directory.
Bildschirmfoto 2025-03-23 um 23 52 58

Running the integration tests with updated package-lock.json (unit tests run here!)

Bildschirmfoto 2025-03-23 um 21 34 53

I would recommend, to add a .nvmrc file and the updated package-lock.json, if we are able to figure out, why the integration tests fail. I had a look into it, but couldn't figure it out yet!?

Let me know, how we should proceed best in your opinion?

@DarioSoller
Copy link
Contributor Author

And I have stumbled over another issue that is kinda confusing me. When these modify mix value calculations are resolved now I end up with slightly different colors than my designer in the tokens-studio Figma Plugin, even though the implementation in the plugin and the implementation in sd-transform look identical?

I need more time to look into this, not sure atm, if it has to do with some rounding errors... but i already wanted to share that I could need some help on this.

@jorenbroekema
Copy link
Member

colorjs.io unfortunately has a couple of versions that are incompatible with certain versions of TypeScript. I've given that feedback to them to check their type interfaces, which they manually create, for compatibility with latest TypeScript + strict settings.

Not sure if they made any progress on this. It's why I had colorjs.io and typescript stuck at pretty specific versions, removing the lock file may have caused those versions to change and this issue to surface again.

As for the rollup-darwin-arm64 thing, yeah it's an NPM bug that surfaces for certain bundlers like Rollup and ESBuild, had it come up in a few projects already. Indeed, as suggested, removing the lockfile and nuking the node_modules solves it, but re-introduces the colorjs.io bug..

I'll try to jump in on this PR and help you fix this nuisance.

I don't want to add a .nvmrc. I don't use NVM. I don't recommend NVM. I want to keep this project agnostic to it. There's an engines property in the package.json that defines compatibility with NodeJS versions, so as long as this is followed, it doesn't matter what Node version manager you end up choosing, NPM install will warn you if you're on an incompatible Node version

Copy link
Member

@jorenbroekema jorenbroekema left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides this one comment and the NVM / package-lock issues, it looks good. If you can try to tackle the fraction digits comment, I'll try to help out on the NVM/package-lock issue and make sure linting / precommit works.

@DarioSoller
Copy link
Contributor Author

DarioSoller commented Mar 26, 2025

I might have found out something regarding color difference in the Figma plugin, compared to sd-transforms output.

The ColorJS.io function toString() also can take the option precision. Relating to the ColorJS.io docs the default value of this property is 5. With precision: 3 I get #C2C5D0 and with precision: 5, which is the default of colorJS.io, I get #C1C5CF. This means I am still slightly off with precision: 3 (#C2C5D0 vs. #C2C6D0), but it is the closest I have been able to reproduce with the actual vendor libraries, that sd-transforms and the Figma plugin are using.

Probably this is the line of code in the plugin, causing this in the plugin. Once again I would really love to see more code sharing between sd-transforms and the Figma Plugin, in order to prevent these unforeseen differences. This also makes me think if my PR here has some implications for the duplicate implementation in the Figma plugin?

I think the best would be that in both, sd-transforms and the Figma plugin the same precision is used, at least the ColorJS.io default precision of 5 or even more.

@DarioSoller
Copy link
Contributor Author

Ok have to correct myself a little bit. As I went even further down this rabbit hole, it now seems that the implementations in the plugin are almost identical:
Figma Plugin:

SD-Transforms:

I have then seen that in the mix.ts the precision is the default one (5), but later on in the modifyColor.ts, the precision is set to 3. I experimented, if chaining several toString() methods with different precisions is causing some weird, unexpected outputs, that my explains the differences we see in the final color values. Unfortunately nothing suspicious could be found that way!? So almost back to beginning now, except, that precision 3 for color mix seems very rough and not efficient enough. I therefore would recommend that everywhere the ColorJs.io toString() method is used, at least the default precision of 5 is used, if not even more?!

Besides that I think we could still use some help in explaining, why we notice these hex color differences between the plugin and the sd-transform library!?

@jorenbroekema
Copy link
Member

jorenbroekema commented Mar 28, 2025

I'm actually not so sure what's causing the differences in the plugin vs sd-transforms but I fully agree that the same implementation should be reused. This is an active topic of discussion in our team atm, I think there's a consensus for reusing but just hasn't been time yet to make it happen.

As for precision, this should probably be configurable and use default of 5, by default, if that's the one that ColorJS uses. But let's tackle this separately.

@jorenbroekema jorenbroekema force-pushed the fix/348-modify-mix-value-calculations branch from 205c8ab to 76dbfba Compare March 28, 2025 09:17
@jorenbroekema jorenbroekema marked this pull request as ready for review March 28, 2025 09:26
@jorenbroekema
Copy link
Member

jorenbroekema commented Mar 28, 2025

Latest colorjs.io works fine with latest typescript, just not with ts-node. I decided to migrate the integration tests away from mocha to vitest. Things are a bit simpler that way in terms of setup. This upgrade to latest colorjs.io may also fix some small issues, I see that the precision of 3 now actually works to be 3 decimals instead of 2, this may already affect your hex values slightly.

Let me know if it's okay to merge this

@DarioSoller
Copy link
Contributor Author

@jorenbroekema Actually I was just working on a backwards compatible way of configuring the color calculation math fraction digits and the color precision setting on different levels (platform config, transform options, or on individual token basis). Let me checkout your update and I will try to bring my adjustments in the tests on the new testing setup. Then i will explain in detail what my thoughts have been and if you are convinced to merge this, or if we just pick parts of it.

Thanks for taking care of the dependency updates.

@DarioSoller DarioSoller marked this pull request as draft March 28, 2025 09:40
@DarioSoller
Copy link
Contributor Author

DarioSoller commented Mar 28, 2025

@jorenbroekema in the meantime, could you think about how the displayed hex color in the Figma plugin for a mix modifier, can be guaranteed to be the value that is actually coming out of the sd-transforms? Otherwise I fear that this will always create confusion for people (incl. myself). I guess this could be solved by simply increasing the the mathFractionDigits and color precision defaults. If someone wants to have less precision by configuring it accordingly in sd-transforms, it should be more intuitive, why the values to the Figma plugin differ, than the other way around.
image (2)

@DarioSoller DarioSoller force-pushed the fix/348-modify-mix-value-calculations branch from 7977955 to 9d40322 Compare March 28, 2025 16:44
@DarioSoller
Copy link
Contributor Author

CI/CD Typescript errors can be solved with PR: tokens-studio/types#49

@DarioSoller DarioSoller marked this pull request as ready for review March 28, 2025 16:47
Copy link
Contributor Author

@DarioSoller DarioSoller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented on some minor details we should check

@DarioSoller DarioSoller marked this pull request as draft March 29, 2025 23:46
Copy link
Contributor Author

@DarioSoller DarioSoller left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jorenbroekema I add a few comments in the code. Things we should consider and think about, as well as explanations of the code changes.

@@ -36,7 +36,7 @@
"@bundled-es-modules/deepmerge": "^4.3.1",
"@bundled-es-modules/postcss-calc-ast-parser": "^0.1.6",
"@tokens-studio/types": "^0.5.1",
Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will have to update the types, after my PR for the @tokens-studio/types is merged.

): Color {
const mixValue = Math.max(0, Math.min(1, Number(amount)));

return new Color(color.mix(mixColor, mixValue, { space: colorSpace }).toString());
return new Color(color.mix(mixColor, mixValue, { space: colorSpace }).toString({ precision }));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taking the actual configured color precision, instead of always falling back to the colorJS.io default precision.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use a single property here, let's reuse mathFractionDigits as well here. They are kinda the same thing, amount of digits after the decimal point for numbers. Specifying a different mathFractionDigits for potential math expressions in color definitions and then also a different precision for the final output doesn't really make sense to me to be honest.

Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just be aware, that you then have a coupling between spacing calculations and color calculations, especially in the color modifiers. I am not 100% sure, if this coupling makes sense? More details in this discussion. But I agree, that it would make things much simpler in the code.

let resolvedMathFractionDigits: number = defaultFractionDigits;
if (modifier?.mathFractionDigits) {
resolvedMathFractionDigits = modifier.mathFractionDigits;
}
Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually modifier.mathFractionDigits will never be undefined here, but I had to initialze resolvedMathFractionDigits that way, because otherwise I would have needed to change the ColorModifyOptions type in such a way, that mathFractionDigits is not optional. More elegant ways of initialization stumbled over 100% code coverage in the tests. Because of backwards compatibility, I think it's fine like this then?

let resolvedColorPrecision: number = defaultColorPrecision;
if (modifier?.precision) {
resolvedColorPrecision = modifier.precision;
}
Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here: Actually modifier. precision will never be undefined here, but I had to initialze resolvedColorPrecision that way, because otherwise I would have needed to change the ColorModifyOptions type in such a way, that precision is not optional. More elegant ways of initialization stumbled over 100% code coverage in the tests. Because of backwards compatibility, I think it's fine like this then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see other comment, I can look into it.

}
if (!modifier.precision) {
modifier.precision = options?.precision ?? defaultColorPrecision;
}
Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If mathFractionDigits or precision are not set on the token level modify, then either use the respective configuration coming from the platform config or the transform options, or fall back to the default values for mathFractionDigits and precision.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should just pass the "options" to modifyColor function, and handle this logic in there. Since there it already has logic that deals with the fallback values. So then it would just be:
const resolvedMathFractionDigits = modifier.mathFractionDigits ?? options?.mathFractionDigits ?? defaultFractionDigits;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will pick this topic up again, as soon as we know more, in which direction ColorJS.io is heading now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would seem to me that we should max out precision for now and then constrain the amount of digits after the decimal period based on mathFractionDigits option that's also used for the resolve math transform. This essentially adds the precision, in the intuitive manner. It feels to me that ColorJS.io will not end up agreeing with my stance on precision, and I'm not that interested in creating a PR for adding a 2nd option if it means that I'm just going to end up turning off precision 😅 I can already do that now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sigh, I just realized during the color modify, it's kinda hard to adjust the amount of decimals, because values are usually strings of some kind of color format... So I guess we do need to either park it until the colorjs.io work is done or we need to just pick a sensible precision for now like 5, and call it a bugfix..

}

return resolvedOpts;
};
Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might have room for improvement, if you have an idea?

First check if mathFractionDigits or precision are set in the platform config. If they are also set in the transform options, then the platform configs get overwritten. So it also supports the case that either one of them is set on the platform level and on the transform option level.

As we now have precision also in the platform level, I think I have to make a PR for updating the platform types in Style-Dictionary? If so it might actually be better to rename precision to colorPrecision, so that it is clear that this only alters the precision with which colors are represented in the output files?

Copy link
Member

@jorenbroekema jorenbroekema Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll take a look at making this a bit more elegant.

I think to summarize we have this order of precedence, from highest prio to least:

  • Token definition -> modify -> precision
  • Platform config -> precision
  • SD-transforms register(SD, options) -> options -> transformName -> precision
  • default values if none of the above is defined

In v6 I want to be able to pass transform options a bit differently, similar to how you do with formats. Then in the above list we can remove 2 and 3 and instead we use the new method of passing transform options.

Copy link
Contributor Author

@DarioSoller DarioSoller Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah right. As I am reading your comment, I might have assumed another order of priority like:

  1. Token definition -> modify -> precision
  2. SD-transforms register(SD, options) -> options -> transformName -> precision
  3. Platform config -> precision
  4. default values if none of the above is defined

But your's makes definitely more sense, as I am thinking of it. Little adjustment needed then in the code: register.ts > resolveColorModifyTransformOpts()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I corrected the order accordingly.

@DarioSoller DarioSoller force-pushed the fix/348-modify-mix-value-calculations branch from 3ca6a91 to fae40dd Compare March 31, 2025 08:38
@DarioSoller DarioSoller marked this pull request as ready for review March 31, 2025 08:38
@DarioSoller DarioSoller force-pushed the fix/348-modify-mix-value-calculations branch from fae40dd to 33b6234 Compare March 31, 2025 09:23
@DarioSoller DarioSoller force-pushed the fix/348-modify-mix-value-calculations branch from 33b6234 to a349914 Compare March 31, 2025 09:24
Comment on lines +45 to +48
let resolvedMathFractionDigits: number = defaultFractionDigits;
if (modifier?.mathFractionDigits) {
resolvedMathFractionDigits = modifier.mathFractionDigits;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let resolvedMathFractionDigits: number = defaultFractionDigits;
if (modifier?.mathFractionDigits) {
resolvedMathFractionDigits = modifier.mathFractionDigits;
}
const resolvedMathFractionDigits = modifier.mathFractionDigits ?? defaultFractionDigits;

If coverage complains then I think we need to add a test to cover the branch that is not covered.
If typescript complains because modifier can be undefined then we need to fix that because it can't be undefined. I'd be really surprised if it thinks it can be, it's not an optional parameter. I can look into it more deeply if it's an issue.


// 3 for backwards compatibility, but should better be 5, referring to the default precision (5) of the colorJS.io library
// see: https://colorjs.io/docs/output.html#getting-a-string-representation-of-a-color
export const defaultColorPrecision = 3;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we now decide to just stick with the defaultColorPrecision of 5, I would recommend, that we still explicitly set this precision, instead of relying on the ColorJS default. As this precision setting has quite some unintuitive impact on (mixed) colors, I would really define it explicitly. Let me know, if the place where I have put constants.ts makes sense for you?

@DarioSoller
Copy link
Contributor Author

Closed in favour of PR #354

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Bug]: Modify Mix not applied for certain modify value calculations/references
2 participants