css-angle.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const {CSS_ANGLEUNIT} = require("devtools/shared/css/properties-db");
  6. const SPECIALVALUES = new Set([
  7. "initial",
  8. "inherit",
  9. "unset"
  10. ]);
  11. const {getCSSLexer} = require("devtools/shared/css/lexer");
  12. /**
  13. * This module is used to convert between various angle units.
  14. *
  15. * Usage:
  16. * let {angleUtils} = require("devtools/client/shared/css-angle");
  17. * let angle = new angleUtils.CssAngle("180deg");
  18. *
  19. * angle.authored === "180deg"
  20. * angle.valid === true
  21. * angle.rad === "3,14rad"
  22. * angle.grad === "200grad"
  23. * angle.turn === "0.5turn"
  24. *
  25. * angle.toString() === "180deg"; // Outputs the angle value and its unit
  26. * // Angle objects can be reused
  27. * angle.newAngle("-1TURN") === "-1TURN"; // true
  28. */
  29. function CssAngle(angleValue) {
  30. this.newAngle(angleValue);
  31. }
  32. module.exports.angleUtils = {
  33. CssAngle: CssAngle,
  34. classifyAngle: classifyAngle
  35. };
  36. CssAngle.ANGLEUNIT = CSS_ANGLEUNIT;
  37. CssAngle.prototype = {
  38. _angleUnit: null,
  39. _angleUnitUppercase: false,
  40. // The value as-authored.
  41. authored: null,
  42. // A lower-cased copy of |authored|.
  43. lowerCased: null,
  44. get angleUnit() {
  45. if (this._angleUnit === null) {
  46. this._angleUnit = classifyAngle(this.authored);
  47. }
  48. return this._angleUnit;
  49. },
  50. set angleUnit(unit) {
  51. this._angleUnit = unit;
  52. },
  53. get valid() {
  54. let token = getCSSLexer(this.authored).nextToken();
  55. if (!token) {
  56. return false;
  57. }
  58. return (token.tokenType === "dimension"
  59. && token.text.toLowerCase() in CssAngle.ANGLEUNIT);
  60. },
  61. get specialValue() {
  62. return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
  63. },
  64. get deg() {
  65. let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
  66. if (invalidOrSpecialValue !== false) {
  67. return invalidOrSpecialValue;
  68. }
  69. let angleUnit = classifyAngle(this.authored);
  70. if (angleUnit === CssAngle.ANGLEUNIT.deg) {
  71. // The angle is valid and is in degree.
  72. return this.authored;
  73. }
  74. let degValue;
  75. if (angleUnit === CssAngle.ANGLEUNIT.rad) {
  76. // The angle is valid and is in radian.
  77. degValue = this.authoredAngleValue / (Math.PI / 180);
  78. }
  79. if (angleUnit === CssAngle.ANGLEUNIT.grad) {
  80. // The angle is valid and is in gradian.
  81. degValue = this.authoredAngleValue * 0.9;
  82. }
  83. if (angleUnit === CssAngle.ANGLEUNIT.turn) {
  84. // The angle is valid and is in turn.
  85. degValue = this.authoredAngleValue * 360;
  86. }
  87. let unitStr = CssAngle.ANGLEUNIT.deg;
  88. if (this._angleUnitUppercase === true) {
  89. unitStr = unitStr.toUpperCase();
  90. }
  91. return `${Math.round(degValue * 100) / 100}${unitStr}`;
  92. },
  93. get rad() {
  94. let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
  95. if (invalidOrSpecialValue !== false) {
  96. return invalidOrSpecialValue;
  97. }
  98. let unit = classifyAngle(this.authored);
  99. if (unit === CssAngle.ANGLEUNIT.rad) {
  100. // The angle is valid and is in radian.
  101. return this.authored;
  102. }
  103. let radValue;
  104. if (unit === CssAngle.ANGLEUNIT.deg) {
  105. // The angle is valid and is in degree.
  106. radValue = this.authoredAngleValue * (Math.PI / 180);
  107. }
  108. if (unit === CssAngle.ANGLEUNIT.grad) {
  109. // The angle is valid and is in gradian.
  110. radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180);
  111. }
  112. if (unit === CssAngle.ANGLEUNIT.turn) {
  113. // The angle is valid and is in turn.
  114. radValue = this.authoredAngleValue * 360 * (Math.PI / 180);
  115. }
  116. let unitStr = CssAngle.ANGLEUNIT.rad;
  117. if (this._angleUnitUppercase === true) {
  118. unitStr = unitStr.toUpperCase();
  119. }
  120. return `${Math.round(radValue * 10000) / 10000}${unitStr}`;
  121. },
  122. get grad() {
  123. let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
  124. if (invalidOrSpecialValue !== false) {
  125. return invalidOrSpecialValue;
  126. }
  127. let unit = classifyAngle(this.authored);
  128. if (unit === CssAngle.ANGLEUNIT.grad) {
  129. // The angle is valid and is in gradian
  130. return this.authored;
  131. }
  132. let gradValue;
  133. if (unit === CssAngle.ANGLEUNIT.deg) {
  134. // The angle is valid and is in degree
  135. gradValue = this.authoredAngleValue / 0.9;
  136. }
  137. if (unit === CssAngle.ANGLEUNIT.rad) {
  138. // The angle is valid and is in radian
  139. gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180);
  140. }
  141. if (unit === CssAngle.ANGLEUNIT.turn) {
  142. // The angle is valid and is in turn
  143. gradValue = this.authoredAngleValue * 400;
  144. }
  145. let unitStr = CssAngle.ANGLEUNIT.grad;
  146. if (this._angleUnitUppercase === true) {
  147. unitStr = unitStr.toUpperCase();
  148. }
  149. return `${Math.round(gradValue * 100) / 100}${unitStr}`;
  150. },
  151. get turn() {
  152. let invalidOrSpecialValue = this._getInvalidOrSpecialValue();
  153. if (invalidOrSpecialValue !== false) {
  154. return invalidOrSpecialValue;
  155. }
  156. let unit = classifyAngle(this.authored);
  157. if (unit === CssAngle.ANGLEUNIT.turn) {
  158. // The angle is valid and is in turn
  159. return this.authored;
  160. }
  161. let turnValue;
  162. if (unit === CssAngle.ANGLEUNIT.deg) {
  163. // The angle is valid and is in degree
  164. turnValue = this.authoredAngleValue / 360;
  165. }
  166. if (unit === CssAngle.ANGLEUNIT.rad) {
  167. // The angle is valid and is in radian
  168. turnValue = (this.authoredAngleValue / (Math.PI / 180)) / 360;
  169. }
  170. if (unit === CssAngle.ANGLEUNIT.grad) {
  171. // The angle is valid and is in gradian
  172. turnValue = this.authoredAngleValue / 400;
  173. }
  174. let unitStr = CssAngle.ANGLEUNIT.turn;
  175. if (this._angleUnitUppercase === true) {
  176. unitStr = unitStr.toUpperCase();
  177. }
  178. return `${Math.round(turnValue * 100) / 100}${unitStr}`;
  179. },
  180. /**
  181. * Check whether the angle value is in the special list e.g.
  182. * inherit or invalid.
  183. *
  184. * @return {String|Boolean}
  185. * - If the current angle is a special value e.g. "inherit" then
  186. * return the angle.
  187. * - If the angle is invalid return an empty string.
  188. * - If the angle is a regular angle e.g. 90deg so we return false
  189. * to indicate that the angle is neither invalid nor special.
  190. */
  191. _getInvalidOrSpecialValue: function () {
  192. if (this.specialValue) {
  193. return this.specialValue;
  194. }
  195. if (!this.valid) {
  196. return "";
  197. }
  198. return false;
  199. },
  200. /**
  201. * Change angle
  202. *
  203. * @param {String} angle
  204. * Any valid angle value + unit string
  205. */
  206. newAngle: function (angle) {
  207. // Store a lower-cased version of the angle to help with format
  208. // testing. The original text is kept as well so it can be
  209. // returned when needed.
  210. this.lowerCased = angle.toLowerCase();
  211. this._angleUnitUppercase = (angle === angle.toUpperCase());
  212. this.authored = angle;
  213. let reg = new RegExp(
  214. `(${Object.keys(CssAngle.ANGLEUNIT).join("|")})$`, "i");
  215. let unitStartIdx = angle.search(reg);
  216. this.authoredAngleValue = angle.substring(0, unitStartIdx);
  217. this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length);
  218. return this;
  219. },
  220. nextAngleUnit: function () {
  221. // Get a reordered array from the formats object
  222. // to have the current format at the front so we can cycle through.
  223. let formats = Object.keys(CssAngle.ANGLEUNIT);
  224. let putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit));
  225. formats = formats.concat(putOnEnd);
  226. let currentDisplayedValue = this[formats[0]];
  227. for (let format of formats) {
  228. if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) {
  229. this.angleUnit = CssAngle.ANGLEUNIT[format];
  230. break;
  231. }
  232. }
  233. return this.toString();
  234. },
  235. /**
  236. * Return a string representing a angle
  237. */
  238. toString: function () {
  239. let angle;
  240. switch (this.angleUnit) {
  241. case CssAngle.ANGLEUNIT.deg:
  242. angle = this.deg;
  243. break;
  244. case CssAngle.ANGLEUNIT.rad:
  245. angle = this.rad;
  246. break;
  247. case CssAngle.ANGLEUNIT.grad:
  248. angle = this.grad;
  249. break;
  250. case CssAngle.ANGLEUNIT.turn:
  251. angle = this.turn;
  252. break;
  253. default:
  254. angle = this.deg;
  255. }
  256. if (this._angleUnitUppercase &&
  257. this.angleUnit != CssAngle.ANGLEUNIT.authored) {
  258. angle = angle.toUpperCase();
  259. }
  260. return angle;
  261. },
  262. /**
  263. * This method allows comparison of CssAngle objects using ===.
  264. */
  265. valueOf: function () {
  266. return this.deg;
  267. },
  268. };
  269. /**
  270. * Given a color, classify its type as one of the possible angle
  271. * units, as known by |CssAngle.angleUnit|.
  272. *
  273. * @param {String} value
  274. * The angle, in any form accepted by CSS.
  275. * @return {String}
  276. * The angle classification, one of "deg", "rad", "grad", or "turn".
  277. */
  278. function classifyAngle(value) {
  279. value = value.toLowerCase();
  280. if (value.endsWith("deg")) {
  281. return CssAngle.ANGLEUNIT.deg;
  282. }
  283. if (value.endsWith("grad")) {
  284. return CssAngle.ANGLEUNIT.grad;
  285. }
  286. if (value.endsWith("rad")) {
  287. return CssAngle.ANGLEUNIT.rad;
  288. }
  289. if (value.endsWith("turn")) {
  290. return CssAngle.ANGLEUNIT.turn;
  291. }
  292. return CssAngle.ANGLEUNIT.deg;
  293. }