Values.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import { EventDispatcher } from 'three';
  2. class Value extends EventDispatcher {
  3. constructor() {
  4. super();
  5. this.domElement = document.createElement( 'div' );
  6. this.domElement.className = 'param-control';
  7. this._onChangeFunction = null;
  8. this.addEventListener( 'change', ( e ) => {
  9. // defer to avoid issues when changing multiple values in the same call stack
  10. requestAnimationFrame( () => {
  11. if ( this._onChangeFunction ) this._onChangeFunction( e.value );
  12. } );
  13. } );
  14. }
  15. setValue( /*val*/ ) {
  16. this.dispatchChange();
  17. return this;
  18. }
  19. getValue() {
  20. return null;
  21. }
  22. dispatchChange() {
  23. this.dispatchEvent( { type: 'change', value: this.getValue() } );
  24. }
  25. onChange( callback ) {
  26. this._onChangeFunction = callback;
  27. return this;
  28. }
  29. }
  30. class ValueNumber extends Value {
  31. constructor( { value = 0, step = 0.1, min = - Infinity, max = Infinity } ) {
  32. super();
  33. this.input = document.createElement( 'input' );
  34. this.input.type = 'number';
  35. this.input.value = value;
  36. this.input.step = step;
  37. this.input.min = min;
  38. this.input.max = max;
  39. this.input.addEventListener( 'change', this._onChangeValue.bind( this ) );
  40. this.domElement.appendChild( this.input );
  41. this.addDragHandler();
  42. }
  43. _onChangeValue() {
  44. const value = parseFloat( this.input.value );
  45. const min = parseFloat( this.input.min );
  46. const max = parseFloat( this.input.max );
  47. if ( value > max ) {
  48. this.input.value = max;
  49. } else if ( value < min ) {
  50. this.input.value = min;
  51. } else if ( isNaN( value ) ) {
  52. this.input.value = min;
  53. }
  54. this.dispatchChange();
  55. }
  56. step( value ) {
  57. this.input.step = value;
  58. return this;
  59. }
  60. addDragHandler() {
  61. let isDragging = false;
  62. let startY, startValue;
  63. this.input.addEventListener( 'mousedown', ( e ) => {
  64. isDragging = true;
  65. startY = e.clientY;
  66. startValue = parseFloat( this.input.value );
  67. document.body.style.cursor = 'ns-resize';
  68. } );
  69. document.addEventListener( 'mousemove', ( e ) => {
  70. if ( isDragging ) {
  71. const deltaY = startY - e.clientY;
  72. const step = parseFloat( this.input.step ) || 1;
  73. const min = parseFloat( this.input.min );
  74. const max = parseFloat( this.input.max );
  75. let stepSize = step;
  76. if ( ! isNaN( max ) && isFinite( min ) ) {
  77. stepSize = ( max - min ) / 100;
  78. }
  79. const change = deltaY * stepSize;
  80. let newValue = startValue + change;
  81. newValue = Math.max( min, Math.min( newValue, max ) );
  82. const precision = ( String( step ).split( '.' )[ 1 ] || [] ).length;
  83. this.input.value = newValue.toFixed( precision );
  84. this.input.dispatchEvent( new Event( 'input' ) );
  85. this.dispatchChange();
  86. }
  87. } );
  88. document.addEventListener( 'mouseup', () => {
  89. if ( isDragging ) {
  90. isDragging = false;
  91. document.body.style.cursor = 'default';
  92. }
  93. } );
  94. }
  95. getValue() {
  96. return parseFloat( this.input.value );
  97. }
  98. }
  99. class ValueCheckbox extends Value {
  100. constructor( { value = false } ) {
  101. super();
  102. const label = document.createElement( 'label' );
  103. label.className = 'custom-checkbox';
  104. const checkbox = document.createElement( 'input' );
  105. checkbox.type = 'checkbox';
  106. checkbox.checked = value;
  107. this.checkbox = checkbox;
  108. const checkmark = document.createElement( 'span' );
  109. checkmark.className = 'checkmark';
  110. label.appendChild( checkbox );
  111. label.appendChild( checkmark );
  112. this.domElement.appendChild( label );
  113. checkbox.addEventListener( 'change', () => {
  114. this.dispatchChange();
  115. } );
  116. }
  117. getValue() {
  118. return this.checkbox.checked;
  119. }
  120. }
  121. class ValueSlider extends Value {
  122. constructor( { value = 0, min = 0, max = 1, step = 0.01 } ) {
  123. super();
  124. this.slider = document.createElement( 'input' );
  125. this.slider.type = 'range';
  126. this.slider.min = min;
  127. this.slider.max = max;
  128. this.slider.step = step;
  129. const numberValue = new ValueNumber( { value, min, max, step } );
  130. this.numberInput = numberValue.input;
  131. this.numberInput.style.width = '60px';
  132. this.numberInput.style.flexShrink = '0';
  133. this.slider.value = value;
  134. this.domElement.append( this.slider, this.numberInput );
  135. this.slider.addEventListener( 'input', () => {
  136. this.numberInput.value = this.slider.value;
  137. this.dispatchChange();
  138. } );
  139. numberValue.addEventListener( 'change', () => {
  140. this.slider.value = parseFloat( this.numberInput.value );
  141. this.dispatchChange();
  142. } );
  143. }
  144. setValue( val ) {
  145. this.slider.value = val;
  146. this.numberInput.value = val;
  147. return super.setValue( val );
  148. }
  149. getValue() {
  150. return parseFloat( this.slider.value );
  151. }
  152. step( value ) {
  153. this.slider.step = value;
  154. this.numberInput.step = value;
  155. return this;
  156. }
  157. }
  158. class ValueSelect extends Value {
  159. constructor( { options = [], value = '' } ) {
  160. super();
  161. const select = document.createElement( 'select' );
  162. const createOption = ( name, optionValue ) => {
  163. const optionEl = document.createElement( 'option' );
  164. optionEl.value = name;
  165. optionEl.textContent = name;
  166. if ( optionValue == value ) optionEl.selected = true;
  167. select.appendChild( optionEl );
  168. return optionEl;
  169. };
  170. if ( Array.isArray( options ) ) {
  171. options.forEach( opt => createOption( opt, opt ) );
  172. } else {
  173. Object.entries( options ).forEach( ( [ key, value ] ) => createOption( key, value ) );
  174. }
  175. this.domElement.appendChild( select );
  176. //
  177. select.addEventListener( 'change', () => {
  178. this.dispatchChange();
  179. } );
  180. this.options = options;
  181. this.select = select;
  182. }
  183. getValue() {
  184. const options = this.options;
  185. if ( Array.isArray( options ) ) {
  186. return options[ this.select.selectedIndex ];
  187. } else {
  188. return options[ this.select.value ];
  189. }
  190. }
  191. }
  192. class ValueColor extends Value {
  193. constructor( { value = '#ffffff' } ) {
  194. super();
  195. const colorInput = document.createElement( 'input' );
  196. colorInput.type = 'color';
  197. colorInput.value = this._getColorHex( value );
  198. this.colorInput = colorInput;
  199. this._value = value;
  200. colorInput.addEventListener( 'input', () => {
  201. const colorValue = colorInput.value;
  202. if ( this._value.isColor ) {
  203. this._value.setHex( parseInt( colorValue.slice( 1 ), 16 ) );
  204. } else {
  205. this._value = colorValue;
  206. }
  207. this.dispatchChange();
  208. } );
  209. this.domElement.appendChild( colorInput );
  210. }
  211. _getColorHex( color ) {
  212. if ( color.isColor ) {
  213. color = color.getHex();
  214. }
  215. if ( typeof color === 'number' ) {
  216. color = `#${ color.toString( 16 ) }`;
  217. } else if ( color[ 0 ] !== '#' ) {
  218. color = '#' + color;
  219. }
  220. return color;
  221. }
  222. getValue() {
  223. let value = this._value;
  224. if ( typeof value === 'string' ) {
  225. value = parseInt( value.slice( 1 ), 16 );
  226. }
  227. return value;
  228. }
  229. }
  230. class ValueButton extends Value {
  231. constructor( { text = 'Button', value = () => {} } ) {
  232. super();
  233. const button = document.createElement( 'button' );
  234. button.textContent = text;
  235. button.onclick = value;
  236. this.domElement.appendChild( button );
  237. }
  238. }
  239. export { Value, ValueNumber, ValueCheckbox, ValueSlider, ValueSelect, ValueColor, ValueButton };
粤ICP备19079148号