-
Notifications
You must be signed in to change notification settings - Fork 1
/
IceEd.html
3461 lines (3079 loc) · 139 KB
/
IceEd.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!--
Ice Ed – The friendly Ice Cream Editor
© 2020 J. Müller
Some notes to developers:
- This tool was designed with the goal in mind to work and being distributed as a single file without requiring any further dependencies, resources or even internet access. Any requests for external resources are considered optional and should not break the usability of the tool in case of failure. I recommend to maintain this guideline to ensure this tool can be used, passed on and extended without being dependent of any additional infrastructure.
- The decision to implement this tool in html/javascript was primarily motivated by the reasoning to lower the barriers for changes and additions as much as possible and having a platform independent GUI available without any externals while I´m personally coming from C++ with rather little experience in web development. So if any portions of the code might look somewhat C-ish or do not meet established best-practices for JS this is the reason why. Please feel free to improve or to rise an issue on GitHub.
Please read carefully the following terms and conditions before you download and/or use this software.
The authors hereby grant you a non-exclusive, non-transferable, free of charge right to copy, modify, merge, publish, distribute, and sublicense the software for the sole purpose of non-commercial usage.
Any use for commercial purposes is prohibited. This includes, without limitation, the production of other artifacts for commercial purposes, incorporation in a commercial product or use in a commercial service.
The software is provided "as is", without warranty of any kind, express or implied, including, but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.
You understand and agree that the authors are under no obligation to provide either maintenance services, update services, notices of latent defects or corrections of defects with regard to the software. The authors nevertheless reserve the right to update, modify or discontinue the software at any time.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the software.
-->
<!DOCTYPE html>
<html lang="en"><head>
<title>Ice Ed – The friendly Ice Cream Editor</title>
<meta charset="utf-8">
<meta name="author" content="J. Mueller">
<meta name="description" content="A user friendly editor to create ice cream recipes and calculate the ideal mixture.">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--background: rgb(255, 255, 255);
--light-grey: rgb(220, 220, 220);
--mid-grey: rgb(153, 153, 153);
--accent: rgb(36, 115, 168);
--accent2: rgb(36, 168, 168);
--contrast: rgb(255, 128, 0);
--light-line: 1px solid var(--light-grey);
--mid-line: 1px solid var(--mid-grey);
--focus-line: 1px solid rgba(36, 115, 168, 0.33); /*rgb according to accent*/
--knob-size: 1em;
}
body{
font-family: 'Book Antiqua', Cambria, Garamond, Georgia, serif;
font-size: 14px;
background-color: var(--background);
}
h1, h2, h3, h4 {
color: var(--accent);
margin-bottom: 0.25em;
}
em {color: var(--contrast);}
strong
{
color: var(--accent);
}
h1 {
font-size: 200%;
}
h2{
font-size: 166%;
}
h3 {
font-size: 133%;
}
p{
margin-top: 0px;
}
ul{
margin-top: 2px;
}
a {
color: var(--accent);
text-decoration: none;
}
#Links a {
color: #000;
}
a:hover, #Links a:hover{
color: var(--accent);
text-decoration:underline;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: inherit;
color: inherit;
}
button:focus,
input:focus,
optgroup:focus,
select:focus,
textarea:focus {
outline: var(--focus-line);
}
button{
color: var(--accent);
border: var(--mid-line);
background-color: var(--light-grey);
}
select:disabled,
button:disabled{
color: var(--mid-grey);
background-color: var(--light-grey);
}
button:hover:enabled
{
border-color: var(--accent);
background-color: var(--background);
box-shadow: 1px 1px 1px 3px var(--light-grey);
}
h2 input
{
font-weight: inherit;
}
/*https://www.cssportal.com/style-input-range/*/
input[type=range] {
-webkit-appearance: none;
background: var(--light-grey);
height: 2px;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
-webkit-appearance: none;
border: 0px;
height: 2px;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
width: var(--knob-size);
height: var(--knob-size);
background: var(--accent);
border: 0px;
border-radius: var(--knob-size);
margin-top: calc( var(--knob-size) * -0.5 );
}
input[type=range]::-moz-range-thumb
{
background: var(--accent);
height: var(--knob-size);
width: var(--knob-size);
border-radius: var(--knob-size);
border-color: var(--accent);
}
table {
text-align: left;
border-collapse: collapse;
}
table th, table td {
padding: 2px 5px;
border-bottom: var(--light-line);
}
thead, tfoot {
font-weight: bold;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
table.layout>tbody>tr>td {
border: none;
}
#tblRecipe thead th {
cursor:default;
}
#tblRecipe thead th:hover
{
color: var(--accent);
}
#tblRecipe *:nth-child(1n+1)>input
{
max-width: 5em;
}
#RecipeData>tbody>tr>td:nth-child(2){
vertical-align: top;
padding-left: 6em;
}
table.layout td:nth-child(1) {
text-align: right;
}
#RecipeHints
{
max-width: 24em;
}
#NotesGraph
{
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: start;
align-content: start;
}
#NotesGraph>div {
flex: 0 0 auto;
margin: 10px;
}
#taRecipeNotes{
border: var(--light-line);
margin: 0px;
}
[contentEditable=true]:empty:not(:focus):before{
color: var(--mid-grey);
content:attr(data-text);
}
.tab {
overflow: hidden;
border: var(--light-line);
background-color: var(--light-grey);
}
/* Style the buttons that are used to open the tab content */
/*.tab button {*/
.tab .tablink, .tab .tabbutton{
font-size: 150%;
background-color: var(--light-grey);
float: left;
border: none;
outline: none;
cursor: pointer;
padding: 14px 16px;
}
/* Change background color of buttons on hover */
.tab .tablink:hover, .tab .tabbutton:hover {
background-color: #eee;
/*color: var(--accent2);
text-shadow: 1px 1px 3px var(--accent2);*/
}
/* Create an active/current tablink class */
.tab .tablink.active {
color: var(--accent);
text-decoration: underline overline;
}
#ToolTabBar .tablink{
font-size: 125%;
padding: 8px;
}
#ModalButtons{
margin: auto;
padding-top: 16px;
}
.tabcontent {
display: none;
padding: 6px 12px;
border: var(--light-line);
border-top: none;
}
.tabcontent div{
margin: 0 auto;
max-width: 100%;
}
#Tools{
padding: 0px 0px;
}
#tblIngredientsList td:nth-child(1n+2)>input {
max-width: 3em;
}
#pacPodTable td:nth-child(1n+2)>input {
max-width: 3.5em;
}
#yolkTable input, #yolkTable select{
width: 8em;
}
#About div
{
max-width: 1024px;
}
#AboutContent>a{
cursor: pointer;
}
.modal
{
display: none;
position: fixed;
z-index: 1; /* topmost */
left: 0;
top: 0;
width: 100%;
height: calc(100% - 1.5em - 4px); /* do not cover footer */
overflow: auto; /*scroll if required */
background-color: rgb(0,0,0); /* fallback color */
background-color: rgba(0,0,0,0.5);
}
.modal-content
{
display: inline-block;
background-color: var(--background);
text-align: center;
padding: 2em;
border: var(--light-line);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.modal-content button
{
font-size: 133%;
background-color: inherit;
padding: 14px 8px;
margin: 0px 4px 0px 4px;
display: table-cell;
}
.modal-content table
{
margin-left: auto;
margin-right: auto;
}
.modal-content h3
{
margin-top: 0px;
}
.ModifiedIndicator
{
background: var(--accent);
border-radius: 50%;
width: var(--knob-size);
height: var(--knob-size);
margin-left: 0.33em;
}
footer {
position: fixed;
bottom: 0px;
left: 0px;
width: 100%;
height: 1.5em;
padding: 2px;
border: var(--light-line);
background-color: var(--light-grey);
}
@media print {
button,
footer,
input[type=range],
.tab,
.noprint
{
display: none;
}
input, textarea, select, .tabcontent
{
border: none;
}
#Recipe>h2:first-of-type {
margin-left: auto;
margin-right: auto;
text-align: center;
}
#RecipeData>tbody>tr>td:nth-child(2){
padding-left: 4em;
}
#NotesGraph
{
display: block;
}
#taRecipeNotes{
border: none;
}
[contentEditable=true]:empty:not(:focus):before{
display: none;
}
#cvFreezingGraph
{
margin-left: -35px;
}
::-webkit-input-placeholder /* multiple pseudo-elementss can not be combined in one rule */
{ /* WebKit browsers */
color: transparent;
}
:-moz-placeholder
{ /* Mozilla Firefox 4 to 18 */
color: transparent;
}
::-moz-placeholder
{ /* Mozilla Firefox 19+ */
color: transparent;
}
:-ms-input-placeholder
{ /* Internet Explorer 10+ */
color: transparent;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance:textfield;
}
}
</style>
</head>
<body>
<div class="tab" id="tabbar">
<button class="tablink" data-tabgrp="main" data-tabid="Recipe">Recipe</button>
<button class="tablink" data-tabgrp="main" data-tabid="Ingredients List">Ingredients List</button>
<button class="tablink" data-tabgrp="main" data-tabid="Tools">Tools</button>
<button class="tablink" data-tabgrp="main" data-tabid="Links">Links</button>
<button class="tabbutton" id="btnDownload" title="Save a copy of Ice Ed including all current ingredient data.">Download</button>
<button class="tablink" data-tabgrp="main" data-tabid="About">Info & FAQ</button>
</div>
<div id="JavscriptWarning" style="font-size: 300%; color:red;">Please enable Javascript to run Ice Ed</div>
<div id="Recipe" class="tabcontent" data-tabgrp="main">
<p>
<button type="button" id="btnNewRecipe" title="Starts the creatio of a new recipe. The current recipe is stored in the recent recipes list as long as the page is not closed.">⭐ New</button>
<button type="button" id="btnStoreAsIngredient" title="Store this mixture with its result values to the ingredients list. Use this feature for bases, preparations and semi-products.">💾 Store as ingredient</button>
<button type="button" id="btnSaveRecipe" title="Stores the current recipe to a file.">💾 Save to file ...</button>
<input type="file" id="inputLoadRecipe" accept=".ier" style="display: none;">
<button type="button" id="btnLoadRecipe" title="Loads a recipe from a file. The current recipe is stored in the recent recipes list as long as the page is not closed.">📁 Load from file ...</button>
<button type="button" id="btnPrintRecipe" title="Opens the browsers print dialog.">🖨️ Print ...</button>
</p>
<h2>
<input type="text" id="edRecipeName" placeholder="Ice Cream Name">
<span id="ModifiedIndicator" class="ModifiedIndicator" title="Recipe contains unsaved modifications."></span>
</h2>
<p>
</p><table class="layout">
<tbody>
<tr> <td>
<label for="tgtSelection">Type</label> </td> <td>
<select id="tgtSelection" title="Select an ice cream type to set the target parameters for the mixture"></select>
<button type="button" id="btnCategorizeRecipe" title="Finds the type of ice cream that matches the current mixture best.">Categorize Recipe</button>
</td> </tr>
<tr> <td> <label for="slServingTemperature">Serving Temperature </label></td> <td> <input id="slServingTemperature" class="slider" min="-35" max="-5" type="range" value="-18" title="The temperature the ice cream is stored at before serving."> <span id="lbServingTemperature"></span> </td> </tr>
<tr> <td> <label for="slHardness">Hardness </label></td> <td> <input id="slHardness" class="slider" min="0" max="90" type="range" value="75" title="The hardness of the mixture at serving temperature is determined by the ratio of frozen water."> <span id="lbHardness"></span> </td> </tr>
<tr> <td> <label for="slOverrun">Overrun </label></td> <td> <input id="slOverrun" class="slider" min="0" max="150" step="1" type="range" value="0.3" title="Overrun describes the percentage of air incorporated during the churning in relation to the unfrozen mixture."> <span id="lbOverrun"></span> </td> </tr>
<tr class="noprint"> <td> <label for="slScoopSize">Scooper Size </label></td> <td> <input id="slScoopSize" class="slider" min="0" max="1" step="1" type="range" value="1" title="Standard scooper sizes are defined by the volume of the bowl. So the scoops volume doubles this size."> <span id="lbScoopSize"></span> </td> </tr>
</tbody></table>
<p></p>
<table class="layout" id="RecipeData">
<tbody><tr>
<td>
<table id="tblRecipe">
<tfoot></tfoot>
</table>
</td>
<td>
<p id="RecipeInfo"></p>
<p id="RecipeHints"></p>
<p class="noprint">
<b>Scale</b><br>
<input name="Amount" placeholder="New Amount" pattern="^[0-9]+([.,][0-9]+)?$" step="any" id="edTargetWeight" style="width: 6em;">
<select id="selTargetWeightMode">
<option>L</option>
<option>g</option>
<option>Scoops</option>
</select><br>
<label style="font-weight: normal;"> <input type="checkbox" id="cbxScaleByIngredient" onclick="ToggleIngredientScale(event);"> by Ingredient</label><br>
<button id="btnScale" title="Adjust the recipes yield by weight, volume, scoops or by the amount of an ingredient.">Scale</button>
</p>
<p class="noprint">
<b>Optimize</b><br>
<button title="Optimize for the center of the target interval for each parameter" id="btnOptimizeMean">Mean</button>
<button title="Optimize to hit the target range somewhere for all parameters" id="btnOptimizeRange">Range</button>
<button title="Recover the recipe state as it was before the previous optimization" id="btnRestoreRecipe">Restore</button>
</p>
<p id="RecipeStack" class="noprint"></p>
</td>
</tr>
</tbody></table>
<div id="NotesGraph">
<div> <div id="taRecipeNotes" data-text="Notes" contentEditable=true></div></div>
<div><canvas id="cvFreezingGraph"></canvas></div>
</div>
</div>
<div id="Ingredients List" class="tabcontent" data-tabgrp="main">
<p>
<button type="button" id="btnSaveIngredients" title="Saves the current ingredient list to a file">💾 Export</button>
<input type="file" id="inputLoadIngredients" accept=".iei" style="display: none;">
<button type="button" id="btnLoadIngredients" title="Loads additional ingredient data from a file. Duplicate ingredients are merged. Conflicting data needs to be resolved manually.">📁 Import</button>
<input type="checkbox" id="cbxOverrideIngredients" class="noprint" checked="" title="Duplicate conflicting ingredients are replaced with imported data, manual conflict resolution is skipped.">
<label for="cbxOverrideIngredients" class="noprint" title="Duplicate conflicting ingredients are replaced with imported data, manual conflict resolution is skipped.">Override existing</label>
</p>
<p>
<input type="text" id="edIngredientFilter" placeholder="Filter" class="noprint">
<button type="button" id="btnClearIngredientsFilter" title="Resets the filter">❌ Clear</button>
</p>
<table id="tblIngredientsList"></table>
</div>
<div id="Tools" class="tabcontent" data-tabgrp="main">
<div class="tab" id="ToolTabBar">
<button class="tablink" data-tabgrp="tools" data-tabid="PAC/POD">PAC & POD</button>
<button class="tablink" data-tabgrp="tools" data-tabid="PacFromMol">PAC from g/mol</button>
<button class="tablink" data-tabgrp="tools" data-tabid="Yolk">Yolk</button>
</div>
<div id="PAC/POD" class="tabcontent" data-tabgrp="tools">
<h3>Calculate PAC & POD</h3>
<div id="PacPodCalculator"></div>
</div>
<div id="PacFromMol" class="tabcontent" data-tabgrp="tools">
<h3>Calculate PAC from g/mol</h3>
<div>
<input id="edGMolCalculator" placeholder="g/mol" type="number" min="0" step="any" pattern="[0-9]+([\.,][0-9]+)?"><br>
<span id="GMolResult"></span>
</div>
</div>
<div id="Yolk" class="tabcontent" data-tabgrp="tools">
<h3>Yolk Calculator</h3>
<div id="YolkCalculator"></div>
</div>
</div>
<div id="Links" class="tabcontent" data-tabgrp="main">
<h2>Links</h2>
</div>
<div id="About" class="tabcontent" data-tabgrp="main">
<div>
<h2>Ice Ed – The friendly Ice Cream Editor</h2>
<p>
<span id="Version"></span><br>
<button type="button" id="btnCheckUpdate">Check for Updates</button><br>
<span id="VersionInfo"></span>
</p>
<b><p id="AboutContent"></p></b>
<h3>Acknowledgments</h3>
<p>Special thanks go to <i>Robert Kneschke</i> from the <a href="https://www.eis-machen.de" target="_blank" rel="external noopener">Eis machen</a>-Blog for his sound expert advice and valuable recommendations on the UI.</p>
<p>Also <a href="https://github.com/eckes" target="_blank" rel="external noopener">Eckes</a> contribution to the browser compatibility deserve to be credited.</p>
<h3>FAQ</h3>
<p><b>What is the meaning of the beta status?</b><br>This tool is not yet in a final state, so you should be aware of two things. </p><ul>
<li>Expect to find some bugs or glitches. If you do, please report them back to me so I can fix them for the first regular release version.</li>
<li>Do <b>not</b> expect compatibility between this version and its file formats and t's successors. Though I will try not to break the backwards compatibility or to provide a conversion feature by all reasonable means, I can not guarantee that the recipe and ingredient data stored with this version will be readable by the next one.</li>
</ul> <p></p>
<p><b>What do I have to consider when creating a recipe?</b><br>Actually not much, just set the desired target type, serving temperature and hardness to configure the target parameters. Then add the ingredients in their required amounts and adjust the mixture according to your wishes. There is only one caveat. Only account ingredients that are actually soluted in the mixture, as only they contribute to the mixtures properties. So for example if you are going to add some chocolate chips to your ice cream you should exclude them from the calculation.</p>
<p><b>The calculation results I get seem to be odd or differ from the results I get using another tool. What should I do?</b><br>This is most likely caused by some erroneous ingredient data. The best way to address this is to trace back the ingredients that are affecting the wrong result values most and double check the data stored for them in the ingredients list.</p>
<p><b>How should the optimization feature be used?</b><br>You need to be aware that the optimization algorithm can only aim for the technical parameters, but does not know anything about taste. In some cases this can mean it could choose to add lots of salt to decrease the freezing point or to remove any stabilizers as it is not aware of their contribution to the texture. So the best way to use the automatic optimization is to use the optimization to get an idea of the proportions of ingredients needed to fulfill the technical requirements and use this as a basis to tweak the parameters the algorithm missed.<br>The optimization comes in two flavors:</p><ul>
<li><i>Mean</i> is the more aggressive approach, trying to adjust the mixture to hit the center of each target parameter range. I recommend to use this algorithm as the first choice.</li>
<li><i>Range</i> is doing a more relaxed optimization, considering it as sufficient if the recipes sums all fall into the targeted parameter range. So it's results are not as spot on as Mean, but in some cases it still yields a better overall result. Use this as a fallback option or in case you prefer to keep the changes on the mixture as small as possible.</li>
</ul><p></p>
<p><b>What is the meaning of the error value?</b><br>The internal calculation is not done for a target value but for a valid range for each value, but all this numbers are hidden from the UI for the sake of simplicity. The error value describes how much the current mixture fails to met the desired range. For example if the target value should be between 90 and 110, this would be a range of 20. If the current value then is 120, meaning it is 10 off the target range, the error value would be 50% (10/20). This approach ensures that the error for missing a large range a little is smaller than missing a small range by the same amount.</p>
<p><b>What are all these abbreviations about?</b><br>
<i>PAC</i> stands for potere anticongelante. It describes the freezing point depression relative to the one of sucrose, which has the value of 100. It is also known as (relative) freezing point depression factor or anti-freezing power.<br>
<i>POD</i> means potere edulcorante and is used to measure the sweetness in relation to sucrose, which is again defined as a value of 100. This value is also known as relative sweetness, sweetening power or sucrose equivalent (SE).<br>
<i>MSNF</i> abbreviates the term milk-solids non-fat and describes the stuff that remains when removing water and fat from dairy products. For ice cream making the lactose and salt contents are the fractions of primary interest in MSNF.<br>
</p>
<p><b>I created a recipe with custom ingredients and would like to share it with someone else. Do I have to pass on my inredients collection as well?</b><br>No, this is not required. All ingredients used are stored as a backup in the recipe file and automatically imported on loading. When the ingredients are already present their values are compared to ensure the calculation relies on the data matching the ingredients acually used for the mixture.</p>
<p><b>What do I have to take care of when I add ingredients?</b><br>The field <i>Solids</i> describes the total solids, including the MSNF part. So basically everything except water and alcohol needs to be considered here.<br>
<i>PAC</i> includes all anti-freezing effects of an ingredient created by sugars, salt or alcohol, with one exception: the salt contained in the MSNF of dairy products is calculated separately for reasons of the algorithm and therefore should not be included in the PAC value.<br>
When importing ingredient data from the FoodData Central database do not forget to complete the missing data. Although Ice Ed is able to load the complete attributes of an ingredient, most records lack some data fields, so you will have to add them manually.<br>
As a last step for creating new ingredients it is recommended to do a sanity check by comparing their values with those of one or two similar items to track any obvious deviations.</p>
<p><b>How can I keep my added ingredients best?</b><br>You can either use the import/export buttons or you can use the Download link to create a local copy of the tool containing the ingredients list with all current changes.</p>
<p><b>Why can´t I use this tool for commercial purposes?</b><br>I made this tool for a target audience of hobbyists and enthusiasts that love making ice cream but do not make a living out of it. In general I would not mind the tool being used by professional gelatieri as well. But please be aware that the development did not only cost me a good amount of time, but also required me to spend some money for research and testing. So before allowing others to earn money with this tool I would like at least to reimburse my expenses. As soon as I break even I will happily alter the license to a more permissive model (Probably it's going to be GPLv3 or MIT license.) and also might consider adding some features for professional use like price per scoop calculation or batch planning.</p>
<p><b>Which upcoming features are currently planned?</b><br>There are some…</p>
<p><b>Will there be any translations available, soon?</b><br>Currently this has no priority for development. However, if there should be a merge request of reasonable quality I surely will not reject it.</p>
<h3>Support & Feedback</h3>
For any feature requests, bug reports or contributions, please visit the project page on <a target="_blank" rel="noopener noreferrer" href="https://github.com/JoernMueller/Ice-Ed">GitHub</a> or leave me a note via <a target="_blank" rel="noopener noreferrer" href="https://forms.gle/YuNispk4LXSiks8B9">Google Forms</a> (no login required).
<h3>Donations</h3>
<p>
If you would like to support the development and maintenance of Ice Ed with a donation please use this button.<br>
</p><form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="cmd" value="_s-xclick">
<input type="hidden" name="hosted_button_id" value="YZSJ4FV9LT2NU">
<input type="image" src="https://www.paypalobjects.com/en_US/DK/i/btn/btn_donateCC_LG.gif" name="submit" title="PayPal - The safer, easier way to pay online!" alt="Donate with PayPal button" border="0">
<!--<img alt="" border="0" src="https://www.paypal.com/en_DE/i/scr/pixel.gif" width="1" height="1" />-->
</form>
<p></p>
<h3>License & Copyright</h3>
<p>© 2020 J. Müller</p>
<p>
Please read carefully the following terms and conditions before you download and/or use this software.<br>
The authors hereby grant you a non-exclusive, non-transferable, free of charge right to copy, modify, merge, publish, distribute, and sublicense the software for the sole purpose of non-commercial usage.<br>
<i>Any use for commercial purposes is prohibited.</i> This includes, without limitation, the production of other artifacts for commercial purposes, incorporation in a commercial product or use in a commercial service.<br>
The software is provided "as is", without warranty of any kind, express or implied, including, but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.<br>
You understand and agree that the authors are under no obligation to provide either maintenance services, update services, notices of latent defects or corrections of defects with regard to the software. The authors nevertheless reserve the right to update, modify or discontinue the software at any time.<br>
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the software.<br>
</p>
</div>
</div>
<div id="Modal" class="modal">
<div class="modal-content">
<div id="ModalContent">
</div>
<p id="ModalButtons"></p>
</div>
</div>
<p style="margin-bottom: 2em;"></p> <!-- adds some space at the bottom to ensure the footer is not hiding any content -->
<footer id="statusBar" class="footer"></footer>
<script>
//=====================================================================================================================================================================
const VERSION = "0.4.0 beta";
const docBackup = getHtmlContent(); // Backup of the document needs to be done first before any modifications are applied to the DOM so it can be used to modify and download the file later on
const decimalSeparator = (1.1).toLocaleString().substring(1, 2);
const RecipeDataColumns = ["Water", "Sugar", "Fat", "MSNF", "Solids", "PAC", "POD"];
const RecipeColumns = ["Name", "Amount", "Scale to", ""].concat( RecipeDataColumns );
const IngredientDataFields = ["Water", "Sugar", "Fat", "MSNF", "Solids", "PAC", "POD", "kcal"];
// replaceAll is currently not everywhere available. Use this polyfill from https://stackoverflow.com/a/14822579
String.prototype.replaceAll = String.prototype.replaceAll || function (find, replace) {
return this.replace(new RegExp(find.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'g'), replace);
};
// Init tab handlers
{
function tabHandler(event)
{
Array.from(document.getElementsByClassName("tablink")).forEach(function(tablink) {
if( tablink.dataset.tabgrp == event.currentTarget.dataset.tabgrp )
tablink.className = tablink.className.replace(" active", "");
});
Array.from(document.getElementsByClassName("tabcontent")).forEach(function(tabcontent) {
if( tabcontent.dataset.tabgrp == event.currentTarget.dataset.tabgrp)
tabcontent.style.display = "none";
});
document.getElementById(event.currentTarget.dataset.tabid).style.display = "block";
event.currentTarget.className += " active";
switch( event.currentTarget.dataset.tabid )
{
case "Recipe":
DisplayRecipe(); // required to add e.g. new items from ingredients list to drop down edits for recipe ingredient items
break;
case "Ingredients List":
DisplayIngredients();
document.getElementById("edIngredientFilter").focus();
document.getElementById("edIngredientFilter").select();
Info(Object. keys(Ingredients). length + " ingredients loaded");
break;
case "Yolk":
InitYolkTable();
break;
}
}
Array.from(document.getElementsByClassName("tablink")).forEach( tablink => {
tablink.onclick=tabHandler;
});
}
class cIngredient
{
// all values are ratios e.g. 30% Fat would be stored as 0.3
// MSNF = Solids - Fat? (F. Borges
// FDP_Salt = MSNF * 2.37 ???
// FDP_M = Lactose * 1.0 ??? -> is Lactose == 1.0 SE
constructor( water = 0.0, sugar = 0.0, fat = 0.0, solids = 0.0, MSNF = 0.0, PAC = 0.0, POD = 0.0, kcal = 0.0 )
{
if( water > 0.0 )
this.Water = water;
if( sugar > 0.0 )
this.Sugar = sugar;
if( fat > 0.0 )
this.Fat = fat;
if( solids > 0.0 )
this.Solids = solids;
if( MSNF > 0.0 )
this.MSNF = MSNF; // Milk Solids Non-Fat
if( Math.abs( PAC ) > 0.0 )
this.PAC = PAC; // potere anticongelante / anti-freezing power / Freezing point depression factor
if( POD > 0.0 )
this.POD = POD; // potere edulcorante / sweetening power
if( kcal > 0.0 )
this.kcal = kcal;
}
copy()
{
var copy = Object.assign(new cIngredient(), this);
return copy;
}
get isSugar() { return this.Sugar >= 0.3 && this.PAC >= 0.5 && !this.isMilkPowder; }
get isMilkPowder() { return this.MSNF > 0.9 && this.Water < 0.05}
}
var Ingredients = {};
// >>> DO NOT EDIT THE INGREDIENT MARKERS! <<<
/*INGREDIENTS_START_MARKER*/
Ingredients = JSON.parse('\
{"Almond Paste (pure)":{"Water":0.0441,"Fat":0.4993,"Sugar":0.0435,"kcal":5.79,"PAC":-0.84,"POD":0.043141000000000006,"Solids":0.9559},\
"Apple":{"Water":0.8556,"Fat":0.0017000000000000001,"Sugar":0.1039,"kcal":0.52,"Solids":0.14439999999999997,"PAC":0.221,"POD":0.171},\
"Apricot":{"Water":0.8634999999999999,"Fat":0.0039000000000000003,"Sugar":0.0924,"kcal":0.48,"Solids":0.13650000000000007,"PAC":0.12224569298245615,"POD":0.09147999999999999},\
"Atomized Glucose DE40":{"Sugar":0.366,"Solids":1,"PAC":0.79,"POD":0.28,"kcal":3.64},\
"Banana":{"Water":0.7491,"Fat":0.0033,"Sugar":0.1223,"kcal":0.89,"PAC":0.2109339210526316,"POD":0.14124499999999998,"Solids":0.2509},\
"Blackberries":{"Water":0.8815000000000001,"Fat":0.0049,"Sugar":0.048799999999999996,"kcal":0.43,"PAC":0.09153961403508772,"POD":0.058104,"Solids":0.11849999999999994},\
"Blueberries":{"Water":0.8421,"Fat":0.0033,"Sugar":0.09960000000000001,"kcal":0.57,"PAC":0.18841416666666666,"POD":0.11975,"Solids":0.15790000000000004},\
"Buffalo Milk":{"Water":0.8339,"Fat":0.0689,"kcal":0.97,"Solids":0.16610000000000003,"MSNF":0.0971,"PAC":0.05},\
"Butter":{"Water":0.1617,"Fat":0.8111,"Sugar":0.0006,"kcal":7.17,"Solids":0.8383,"PAC":0.01,"POD":0.001},\
"Buttermilk":{"Water":0.8791,"Fat":0.0331,"Sugar":0.048799999999999996,"kcal":0.62,"Solids":0.12090000000000001,"MSNF":0.08789999999999999,"PAC":0.035,"POD":0.005600000000000001},\
"Buttermilk, light":{"Water":0.9013,"Fat":0.0088,"Sugar":0.0479,"kcal":0.4,"Solids":0.09870000000000001,"MSNF":0.09,"PAC":0.035,"POD":0.005600000000000001},\
"Carboxymethyl Cellulose":{"Solids":1,"POD":0.1,"kcal":1.43},\
"Chocolate, dark":{"Water":0.0075,"Fat":0.332,"Sugar":0.48810000000000003,"kcal":5.28,"Solids":0.9925,"PAC":-0.239,"POD":0.488},\
"Cocoa Powder":{"Water":0.03,"Fat":0.13699999999999998,"Sugar":0.005,"kcal":2.28,"Solids":0.97,"PAC":-1.6,"POD":0.005},\
"Coconut Milk":{"Water":0.9457,"Fat":0.0208,"Sugar":0.025,"kcal":0.31,"Solids":0.054300000000000015},\
"Coffee Beans":{"Solids":1,"kcal":0.01},\
"Condensed Milk Sweet":{"Water":0.2716,"Fat":0.087,"Sugar":0.544,"kcal":3.21,"Solids":0.7283999999999999,"PAC":0.58,"POD":0.46},\
"Corn Starch":{"kcal":3.81,"Solids":1},\
"Corn Syrup DE42":{"Water":0.2,"Sugar":0.78,"Solids":0.8,"PAC":0.8,"POD":0.48,"kcal":2.86},\
"Cream 30%":{"Water":0.64,"Sugar":0.032,"Fat":0.3,"Solids":0.363,"MSNF":0.063,"PAC":0.03,"POD":0.0048,"kcal":2.88},\
"Cream, heavy":{"Water":0.5771000000000001,"Fat":0.3608,"Sugar":0.0292,"kcal":3.4,"PAC":0.02922561403508772,"POD":0.004672,"Solids":0.42289999999999994,"MSNF":0.06209999999999993},\
"Cream, light":{"Water":0.7451000000000001,"Fat":0.191,"Sugar":0.036699999999999997,"kcal":1.91,"Solids":0.2548999999999999,"MSNF":0.064,"PAC":0.03,"POD":0.0048},\
"Dextrose":{"Sugar":0.915,"Solids":1,"PAC":1.9,"POD":0.7,"kcal":3.66},\
"Dried Buttermilk Powder (sweet)":{"Water":0.0297,"Fat":0.057800000000000004,"Sugar":0.49,"kcal":3.87,"Solids":0.9703,"MSNF":0.96,"PAC":0.49,"POD":0.078},\
"Dried Skimmed Milk Powder":{"Water":0.02,"Sugar":0.515,"Fat":0.009,"Solids":0.96,"MSNF":0.954,"PAC":0.52,"POD":0.0835,"kcal":3.55},\
"Egg Yolk":{"Water":0.5231,"Fat":0.26539999999999997,"Solids":0.4769,"kcal":3.22,"Sugar":0.005600000000000001,"PAC":0.008186561403508773,"POD":0.003948000000000001,"MSNF":0.21150000000000002},\
"Erythritol":{"Sugar":0.45,"Solids":1,"PAC":2.8,"POD":0.7,"kcal":0.2},\
"Fructose":{"Solids":1,"PAC":1.9,"POD":1.7,"kcal":3.98},\
"Goat Milk":{"Water":0.8703,"Fat":0.0414,"Sugar":0.044500000000000005,"kcal":0.69,"Solids":0.12970000000000004,"MSNF":0.08839999999999999,"PAC":0.04},\
"Grapefruit":{"Water":0.9089,"Fat":0.001,"Sugar":0.0698,"kcal":0.32,"Solids":0.09109999999999996,"PAC":0.098,"POD":0.075},\
"Grapes":{"Water":0.8429000000000001,"Fat":0.004699999999999999,"kcal":0.57,"Solids":0.1570999999999999,"PAC":0.293,"POD":0.19},\
"Guar":{"Water":0.15,"Fat":0.005,"Solids":0.85,"kcal":3.32},\
"Half and Half":{"Water":0.8057,"Fat":0.115,"Sugar":0.041299999999999996,"kcal":1.31,"PAC":0.04133622807017544,"POD":0.006607999999999999,"Solids":0.19430000000000003,"MSNF":0.07930000000000002},\
"Hazelnut Paste (pure)":{"Water":0.053099999999999994,"Fat":0.6075,"Sugar":0.0434,"kcal":6.28,"Solids":0.9469,"PAC":-0.91,"POD":0.042},\
"Honey":{"Water":0.171,"Sugar":0.8212,"kcal":3.04,"Solids":0.829,"PAC":1.8,"POD":1.3},\
"Inulin":{"Sugar":0.1,"Solids":1,"POD":0.1,"kcal":1.4},\
"Invert Syrup 80%":{"Water":0.2,"Sugar":0.8,"Solids":0.8,"PAC":1.34,"POD":1.04,"kcal":3.24},\
"Kiwi":{"Water":0.8306999999999999,"Fat":0.0052,"Sugar":0.08990000000000001,"kcal":0.61,"Solids":0.16930000000000012,"PAC":0.223,"POD":0.14800000000000002},\
"Lambda Carrageenan":{"Solids":1,"POD":0.1,"kcal":1.12},\
"Lecithin":{"Solids":1,"kcal":8.84},\
"Lemon":{"Water":0.8898,"Fat":0.003,"Sugar":0.025,"kcal":0.29,"Solids":0.11019999999999996,"PAC":0.047,"POD":0.042},\
"Lemon Juice":{"Water":0.9231,"Sugar":0.0252,"Fat":0.0024,"Solids":0.07689999999999997,"PAC":0.04404483333333334,"POD":0.029930000000000002,"kcal":0.22},\
"Lime":{"Water":0.8826,"Fat":0.002,"Sugar":0.0169,"kcal":0.3,"Solids":0.11739999999999995,"PAC":0.032,"POD":0.028999999999999998},\
"Litchis":{"Water":0.8176000000000001,"Fat":0.0044,"Sugar":0.1523,"kcal":0.66,"Solids":0.1823999999999999,"PAC":0.287,"POD":0.221},\
"Locust Bean Gum":{"Water":0.15,"Fat":0.005,"Solids":0.85,"kcal":3.32},\
"Maltodextrin":{"Solids":1,"PAC":0.29,"POD":0.17,"kcal":3.8},\
"Mango":{"Water":0.8345999999999999,"Fat":0.0038,"Sugar":0.1366,"kcal":0.6,"Solids":0.1654000000000001,"PAC":0.19692149999999997,"POD":0.16332999999999998},\
"Maple Syrup":{"Water":0.3239,"Fat":0.0006,"Sugar":0.6046,"kcal":2.6,"Solids":0.6760999999999999,"PAC":0.605,"POD":0.605},\
"Milk Chocolate":{"Water":0.015,"Fat":0.2966,"Sugar":0.515,"kcal":5.35,"Solids":0.985,"PAC":0.135,"POD":0.53,"MSNF":0.16},\
"Oranges":{"Water":0.867,"Fat":0.0015,"Sugar":0.0857,"kcal":0.47,"Solids":0.133,"PAC":0.125193,"POD":0.09616},\
"Papaya":{"Water":0.8806,"Fat":0.0026,"Sugar":0.0782,"kcal":0.43,"PAC":0.14871033333333333,"POD":0.09204000000000001,"Solids":0.11939999999999995},\
"Passion Fruit":{"Water":0.7293000000000001,"Fat":0.006999999999999999,"Sugar":0.133,"kcal":0.97,"Solids":0.27069999999999994,"PAC":0.214,"POD":0.146},\
"Peach":{"Water":0.883,"Fat":0.0027,"Sugar":0.0839,"kcal":0.42,"Solids":0.11699999999999999,"PAC":0.11571970175438596,"POD":0.087918},\
"Pineapple":{"Water":0.86,"Fat":0.0012,"Sugar":0.09849999999999999,"kcal":0.5,"PAC":0.13311416666666667,"POD":0.10805,"Solids":0.14},\
"Pistachio Paste (pure)":{"Water":0.0437,"Fat":0.4532,"Sugar":0.0766,"kcal":5.6,"Solids":0.9563,"PAC":-0.7,"POD":0.075},\
"Raspberries":{"Water":0.8575,"Fat":0.006500000000000001,"Sugar":0.044199999999999996,"kcal":0.52,"PAC":0.08206016666666668,"POD":0.05497,"Solids":0.14249999999999996},\
"Rum 40%":{"Water":0.6659999999999999,"kcal":2.31,"Solids":1.1102230246251565e-16,"PAC":2.97},\
"Salt":{"Water":0.002,"Solids":0.998,"PAC":5.86},\
"Skim Milk":{"Water":0.9079999999999999,"Fat":0.0008,"Sugar":0.050499999999999996,"kcal":0.34,"Solids":0.09200000000000008,"PAC":0.05054429824561404,"POD":0.008079999999999999,"MSNF":0.09120000000000009},\
"Sour Cream":{"Water":0.7306999999999999,"Fat":0.1935,"Sugar":0.0341,"kcal":1.98,"Solids":0.2693000000000001,"MSNF":0.075,"PAC":0.032,"POD":0.0051},\
"Strawberries":{"Water":0.9109999999999999,"Sugar":0.053399999999999996,"Solids":0.08900000000000008,"PAC":0.10055716666666667,"POD":0.098,"kcal":0.31,"Fat":0.0022},\
"Sucrose":{"Sugar":0.998,"Solids":0.9998,"PAC":0.9985705,"POD":0.998189,"kcal":3.85,"Water":0.0002,"Fat":0.0032},\
"Trehalose":{"Sugar":0.45,"Solids":1,"PAC":1,"POD":0.45,"kcal":3.62},\
"Vanilla Extract":{"Water":0.5257999999999999,"Fat":0.0006,"Sugar":0.1265,"kcal":2.88,"Solids":0.1302000000000001,"PAC":0.127,"POD":0.127},\
"Vodka 40%":{"Water":0.6,"PAC":2.97,"kcal":2.31},\
"Water":{"Water":1},\
"Whole Milk 3.5%":{"Water":0.8813,"Sugar":0.050499999999999996,"Fat":0.035,"Solids":0.11870000000000003,"MSNF":0.087,"PAC":0.04,"kcal":0.61,"POD":0.0077},\
"Xanthan Gum":{"Solids":1,"kcal":0.54},\
"Yogurt 3.5%":{"Water":0.86,"Sugar":0.035,"Fat":0.035,"Solids":0.14,"MSNF":0.142,"PAC":0.071,"POD":0.0048,"kcal":0.59}}');
/*INGREDIENTS_END_MARKER*/
for( const key in Ingredients)
Ingredients[key] = Object.assign(new cIngredient(), Ingredients[key]);
function IngredientNames()
{
return Object.keys(Ingredients);
}
function SortIngredients()
{
const keys = Object.keys(Ingredients).sort();
var tmp = {};
for( const key of keys )
{
tmp[key] = Ingredients[key];
}
Ingredients = tmp;
}
SortIngredients();
class cTargetValue
{
constructor(min, max)
{
this.Min = min;
this.Max = max;
this.Range = max - min;
this.Mean = (min + max) * 0.5;
}
getRangeError(factor, value)
{
if(this.Range <= 0.0)
return getMeanError(factor, value);
return Math.max( Math.abs(this.Mean * factor - value) - this.Range * factor * 0.5, 0.0 ) / (this.Range * factor);
};
getMeanError(factor, value)
{
const delta = Math.abs(this.Mean * factor - value);
if( delta == 0.0 )
return 0.0;
return delta / (this.Mean * factor);
}
}
class cTarget
{
constructor( fatMin, fatMax
, msnfMin, msnfMax
, podMin, podMax
, stabilizerMin, stabilizerMax
, solidsMin, solidsMax
)
{
this.Fat = new cTargetValue(fatMin, fatMax);
this.MSNF = new cTargetValue(msnfMin, msnfMax);
this.Solids = new cTargetValue(solidsMin, solidsMax);
this.POD = new cTargetValue(podMin, podMax);
this.Stabilizer = new cTargetValue(stabilizerMin, stabilizerMax);
}
}
var Targets = {};
// Based on: https://www.sciencedirect.com/topics/food-science/frozen-dessert
// Name Fat MSNF POD Stabilizer Total Solids
Targets["Non-Fat"] = new cTarget(0, 0.005, 0.12, 0.14, 0.18, 0.22, 0.009, 0.011, 0.28, 0.32);
Targets["Low Fat"] = new cTarget(0.02, 0.05, 0.12, 0.14, 0.18, 0.21, 0.007, 0.009, 0.28, 0.32);
Targets["Light"] = new cTarget(0.05, 0.07, 0.11, 0.12, 0.18, 0.2, 0.004, 0.006, 0.3, 0.35);
Targets["Reduced Fat"] = new cTarget(0, 0.1, 0.09, 0.1, 0.14, 0.17, 0.002, 0.004, 0.36, 0.38);
Targets["Standard"] = new cTarget(0.10, 0.12, 0.09, 0.1, 0.14, 0.17, 0.002, 0.004, 0.36, 0.38 );
Targets["Premium"] = new cTarget(0.12, 0.14, 0.08, 0.1, 0.13, 0.16, 0.002, 0.004, 0.38, 0.4);
Targets["Super-Premium"] = new cTarget(0.14, 0.18, 0.05, 0.08, 0.14, 0.17, 0.0, 0.002, 0.4, 0.42);
Targets["Gelato"] = new cTarget(0.04, 0.08, 0.09, 0.12, 0.15, 0.24, 0.004, 0.005, 0.32, 0.42);
Targets["Frozen Yogurt: Non-Fat"] = new cTarget(0, 0.005, 0.09, 0.14, 0.15, 0.17, 0.005, 0.007, 0.28, 0.32);
Targets["Frozen Yogurt: Regular"] = new cTarget(0.03, 0.06, 0.09, 0.13, 0.15, 0.17, 0.004, 0.005, 0.3, 0.36);
Targets["Sorbet"] = new cTarget(0.0, 0.01, 0.0, 0.0, 0.22, 0.28, 0.004, 0.005, 0.28, 0.34);
Targets["Sherbet"] = new cTarget(0.01, 0.02, 0.01, 0.03, 0.22, 0.28, 0.004, 0.005, 0.28, 0.34);
// This method must be located below the ingredient markers to ensure they are located properly with the string search
document.getElementById("btnDownload").onclick = () => {
const start_marker = "/*INGREDIENTS_START_MARKER*/";
const end_marker = "/*INGREDIENTS_END_MARKER*/";
const pos_start = docBackup.indexOf(start_marker);
const pos_end = docBackup.indexOf(end_marker);
if( pos_start < 0 || pos_end < 0 || pos_start >= pos_end )
{
ErrorMsg("Failed to locate ingredient markers in document. Download aborted.");
return;
}
const string = docBackup.slice(0, pos_start + start_marker.length)
+ "\nIngredients = JSON.parse('\\\n" + JSON.stringify(Ingredients, (key, value) => { return value == 0.0 ? undefined : value; }).replaceAll('},', "},\\\n") + "');\n"
+ docBackup.slice(pos_end);
var link = document.createElement( 'a' );
link.setAttribute( 'href', URL.createObjectURL( new Blob( [ string ], {type: 'text/html'})));
link.setAttribute( 'download', "IceEd.html" );
clickOn(link);
};
// --- Recipe -----------------------------------------------------------
var RecipeBackup = []; // backups recipe states on optimization
var RecipeStack = {}; // keeps previous recipes when loading or creating new recipes
var sortBy = null;
var sortAsc = false;
document.getElementById("JavscriptWarning").style = "display: none;";
var slServingTemperature = document.getElementById("slServingTemperature");
slServingTemperature.value = -18;
slServingTemperature.oninput = function()
{
document.getElementById("lbServingTemperature").innerHTML = this.value + " °C";
Recipe.ServingTemperature = toFloat(this.value);
SetRecipeModified();
UpdateRecipeSums();
};
var slHardness = document.getElementById("slHardness");
slHardness.value = 75;
slHardness.oninput = function()
{
document.getElementById("lbHardness").innerHTML = this.value + " %";
Recipe.Hardness = toFloat(this.value) / 100.0;
SetRecipeModified();
UpdateRecipeSums();
};
var slOverrun = document.getElementById("slOverrun");
slOverrun.value = 0.3;
slOverrun.oninput = function()
{
var overrun = toFloat(this.value) / this.max ;
overrun *= overrun * 1.5;
document.getElementById("lbOverrun").innerHTML = Math.round(overrun*100.) + " %";
Recipe.Overrun = overrun;
SetRecipeModified();
UpdateRecipeInfo();
};
var scoopSizes = [];
const sccopsPerQuart = [4, 5, 6, 8, 10, 12, 14, 16, 20, 24, 30, 36, 40, 50, 60, 70, 100];
const scoopsPerLiter = [4, 5, 6, 7, 8, 9, 10, 12, 16, 20, 22, 24, 30, 36, 40, 45, 50, 55, 60, 65, 70, 85, 100];
for(const v of scoopsPerLiter)
{
const ccmm = 1000000. / v;
scoopSizes.push({
Size: 2. * Math.cbrt(6.*ccmm/(4.*Math.PI)),
ML: ccmm * 0.002,
LabelHTML: "<sup>1</sup>⁄<sub>" + v + "</sub> L"
});
}
for(var v of sccopsPerQuart)
{
const ccmm = 0.946353 * 1000000. / v;
scoopSizes.push({
Size: 2. * Math.cbrt(6.*ccmm/(4.*Math.PI)),
ML: ccmm * 0.002,
LabelHTML: "<sup>1</sup>⁄<sub>" + v + "</sub> qt"
});
}
scoopSizes.sort((a,b) => {return a.Size > b.Size ? 1 : -1;});
var slScoopSize = document.getElementById("slScoopSize");
slScoopSize.max = scoopSizes.length - 1;
slScoopSize.value = 24;
slScoopSize.oninput = () =>
{
const index = toFloat(slScoopSize.value);
document.getElementById("lbScoopSize").innerHTML =
'<table class="layout" style="display:inline;""><tbody><tr>'
+ '<td style="width: 3.75em; padding-top: 0px; text-align: left;">' + scoopSizes[index].LabelHTML + '</td>'
+ '<td style="color: var(--mid-grey); padding-top: 0px;">' + Math.round(scoopSizes[index].Size) + " mm " + Math.round(scoopSizes[index].ML) + ' ml</td>'
+ '</tr></tbody></table>';
UpdateRecipeInfo();
};
var tgtSelection = document.getElementById('tgtSelection');
for( const key in Targets )
{
var option = document.createElement('option');
option.value =
option.text = key;
tgtSelection.appendChild(option);
}
tgtSelection.onchange = () =>
{
Recipe.Type = tgtSelection.value;
UpdateRecipeSums();
SetRecipeModified();
};
class cRecipe
{
constructor(name = "", notes = "")
{