-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcalcteach.py
More file actions
1568 lines (1282 loc) · 52.4 KB
/
Copy pathcalcteach.py
File metadata and controls
1568 lines (1282 loc) · 52.4 KB
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
import math
from typing import Callable
from manim import *
from manim import ValueTracker, always_redraw, linear
from manim.utils.tex import TexTemplate
from pydub import AudioSegment
import numpy as np
import sympy as sp
from sympy import Function, Mul
from sympy.calculus.util import continuous_domain
'''
cd ~/projects/calcteach
source .venv/bin/activate
manim calcteach.py SkibidiToilet
rm -rf media/videos/calcteach/*/partial_movie_files/SkibidiToilet
'''
# display
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 60
# font, color
DOTCOLOR = RED
GRAPHCOLOR = YELLOW
FRUSTUMCOLOR = GRAY_BROWN
CHORDCOLORS = [
RED, PURPLE, TEAL,
RED_D, PURPLE_D, TEAL_D,
RED_B, PURPLE_B, TEAL_B
]
DXCOLOR = GREEN
DYCOLOR = BLUE
DSCOLOR = RED
FPRIMECOLOR = ORANGE
TANGENTLEN = 2
FPRIMELABELOFFSET = 1.5
SACOLOR = LIGHT_BROWN
BOUNDCOLORS = (RED, BLUE)
FONTSIZE = 42
LABELSIZE = 36
STROKEWIDTH = 2
# f(x)
FUNCTION = "sqrt(x)"
DOMAIN = (1, 4)
XSEGSIZE = 0.5
# rotation
NUMFACES = (64, 128) # along (x axis, circular path)
OPACITY = 0.5
# formula animation
PYTHAGZOOMMOD = 1.4
WAITCAMERA = 0.8
WAITFUNC = 2
RUNTIME = 0.69
RTFACTOR = 3.0
FORMULASHIFT = 1
# kinda useless and will never change anyways
MAXONSCREEN = 3
# graph
MINTICKS = 3
MAXTICKS = 10
XTICKSTEP = 1
XGRAPHSIZE = 13
YGRAPHSIZE = 4.5
# accuracy
TICKRANGEACC = 32
TOLERANCE = 1e-9
# camera angle
NORMANGLE = dict(phi = 0 * DEGREES, theta = -90 * DEGREES, zoom = 0.9)
SEXYANGLE = dict(phi = 55 * DEGREES, theta = -45 * DEGREES, zoom = 0.75)
# music (not used vro)
MUSICPATH = "/Users/nguyen/projects/calcteach/music.wav"
FADEOUTTIME = 5
GAIN = (0, -60) # endgain is not used
def guard_divisions(expr: sp.Expr) -> sp.Expr:
# recurse first
if not expr.args:
return expr
expr = expr.func(*(guard_divisions(arg) for arg in expr.args))
# 1) pure Pow with exp<0
if expr.is_Pow and expr.exp < 0:
# turn x**-n into SafeDiv(1, x**n)
# CHANGE THIS LINE:
return SafeDiv(1, expr.base**(-expr.exp)) # Correctly use -expr.exp
# 2) Mul containing exact exp == -1
if expr.is_Mul:
nums, dens = [], []
for arg in expr.args:
if arg.is_Pow and arg.exp == -1:
dens.append(arg.base)
else:
nums.append(arg)
if dens:
A = Mul(*nums) if len(nums) > 1 else nums[0]
B = Mul(*dens) if len(dens) > 1 else dens[0]
return SafeDiv(A, B)
return expr
def safeDiv(a: float, b: float) -> float:
"""
Divide a by b, but if |b| < eps, use eps (with sign of b) instead.
"""
if abs(b) < 1e-6:
b = 1e-6 if b >= 0 else -1e-6
return a / b
def defineSafeFunc(eStr: str, sym: str) -> tuple[Callable, sp.Expr]:
# parse and rewrite
raw = eStr.replace("^", "**")
var = sp.symbols(sym)
expr = sp.sympify(raw)
guarded = guard_divisions(expr)
# now lambdify, telling it how to implement SafeDiv
func = sp.lambdify(
var,
guarded,
modules=[{"SafeDiv": safeDiv}, "numpy"]
)
return func, guarded
# idk how this works but it works
SafeDiv = Function("SafeDiv")
# f(x) and range of f(x)
f, guarded_expr = defineSafeFunc(FUNCTION, "x")
# f'(x)
diffExpr = sp.sympify(FUNCTION.replace("^", "**"))
derivative = sp.diff(diffExpr, "x")
fprime, _ = defineSafeFunc(str(derivative), "x")
func_latex = sp.latex(diffExpr)
# f'(x) latex thing
x_sym = sp.symbols("x")
diff_symbolic = sp.diff(diffExpr, x_sym)
diff_latex = sp.latex(diff_symbolic)
# checks if f(x) differentiable over DOMAIN
def check_function_differentiability(
func_str: str,
domain_tuple: tuple[float, float],
x_seg_size_for_numeric: float,
tolerance_for_numeric: float
) -> None:
"""
Checks if the function defined by func_str is differentiable over domain_tuple.
Raises ValueError if non-differentiable points are detected.
"""
x_sym_check = sp.symbols("x")
try:
func_expr_check = sp.sympify(func_str.replace("^", "**"))
raw_derivative_expr_check = sp.diff(func_expr_check, x_sym_check)
except Exception as e_sympify:
raise ValueError(
f"Could not parse FUNCTION '{func_str}' for differentiability check. Error: {e_sympify}"
)
sympy_domain_check = sp.Interval(domain_tuple[0], domain_tuple[1])
problem_points_reports = []
symbolic_check_performed_successfully = False
# --- Symbolic Check ---
try:
# Using sympy.calculus.util.continuous_domain
deriv_cont_domain_check = continuous_domain(
raw_derivative_expr_check, x_sym_check, sympy_domain_check
)
problematic_set_check = sympy_domain_check - deriv_cont_domain_check
if problematic_set_check != sp.EmptySet:
current_problem_reports = []
if problematic_set_check.is_FiniteSet:
current_problem_reports = [str(p) for p in list(problematic_set_check)]
elif problematic_set_check.is_Union:
for arg in problematic_set_check.args:
if arg.is_FiniteSet:
current_problem_reports.extend([str(p) for p in list(arg)])
else:
current_problem_reports.append(str(arg))
else:
current_problem_reports.append(str(problematic_set_check))
if current_problem_reports:
problem_points_reports = current_problem_reports # Store for potential error
# Do not raise error yet, allow numerical check to confirm or add info if needed.
# However, if symbolic check is definitive, one might choose to raise here.
# For this implementation, we collect info and make a decision after numerical check.
symbolic_check_performed_successfully = True # Mark as performed
except NotImplementedError:
print(f"Warning: Symbolic differentiability check (continuous_domain) for derivative "
f"'{sp.latex(raw_derivative_expr_check)}' resulted in NotImplementedError. "
f"Proceeding to numerical check.")
except Exception as e_symbolic:
print(f"Warning: Error during symbolic differentiability check: {e_symbolic}. "
f"Proceeding to numerical check.")
# --- Numerical Check ---
# Runs if symbolic check did not raise an error, or if it failed/was skipped.
numerical_issue_points = []
try:
modules_for_lambdify = ["numpy"]
try:
import scipy # type: ignore
modules_for_lambdify.append("scipy")
except ImportError:
pass
check_fprime_func_numeric = sp.lambdify(
x_sym_check, raw_derivative_expr_check, modules=modules_for_lambdify
)
domain_width = domain_tuple[1] - domain_tuple[0]
if abs(domain_width) < tolerance_for_numeric: # Effectively a point domain
test_points_numeric = np.array([float(domain_tuple[0])])
else:
num_test_points_numeric = min(1000, 100 + int(abs(domain_width) / x_seg_size_for_numeric) * 10)
num_test_points_numeric = max(2, num_test_points_numeric) # ensure at least 2 points for an interval
test_points_numeric = np.linspace(float(domain_tuple[0]), float(domain_tuple[1]), num_test_points_numeric)
if domain_tuple[0] < 0 < domain_tuple[1] and not np.any(np.isclose(test_points_numeric, 0.0)):
test_points_numeric = np.sort(np.append(test_points_numeric, 0.0))
for x_val_num in test_points_numeric:
try:
deriv_val_num = check_fprime_func_numeric(x_val_num)
if not np.isfinite(deriv_val_num):
numerical_issue_points.append(x_val_num)
except (ZeroDivisionError, OverflowError, ValueError):
numerical_issue_points.append(x_val_num)
if numerical_issue_points:
unique_numerical_issues_str = []
# Simple filtering for unique points for reporting
if numerical_issue_points:
# Round to group very close points, then convert to set for uniqueness, then sort
sorted_num_pts_rounded = sorted(list(set(round(p, 6) for p in numerical_issue_points)))
if sorted_num_pts_rounded:
unique_numerical_issues_str.append(str(sorted_num_pts_rounded[0]))
for i in range(1, len(sorted_num_pts_rounded)):
if abs(sorted_num_pts_rounded[i] - sorted_num_pts_rounded[i-1]) > 1e-5:
unique_numerical_issues_str.append(str(sorted_num_pts_rounded[i]))
if unique_numerical_issues_str:
# If symbolic check already found issues, append numerical findings. Otherwise, use numerical.
if problem_points_reports: # Symbolic check found issues
report_message_addition = (f" Numerical check also found issues at/near x = "
f"{', '.join(unique_numerical_issues_str[:10])}.")
else: # Symbolic check clean or failed, rely on numerical
problem_points_reports = [
f"Numerical check of derivative '{sp.latex(raw_derivative_expr_check)}' "
f"yielded non-finite values or errors at/near x = "
f"{', '.join(unique_numerical_issues_str[:10])}."
]
report_message_addition = "" # Message is self-contained
# Raise error based on combined or numerical findings
raise ValueError(
f"Function '{func_str}' may not be differentiable across domain {domain_tuple}. "
f"{' '.join(problem_points_reports)}{report_message_addition}"
)
except Exception as e_numerical_setup:
# This catches errors in setting up/running the numerical check itself
# If symbolic check was successful and found no issues, we might trust it.
# But if symbolic check also failed or was skipped, this is problematic.
if not symbolic_check_performed_successfully or not problem_points_reports:
raise RuntimeError(
f"Differentiability check for function '{func_str}' on domain {domain_tuple} was inconclusive. "
f"Symbolic check may have been skipped or failed. "
f"Numerical check setup/execution also failed with error: {e_numerical_setup}"
)
elif symbolic_check_performed_successfully and not problem_points_reports:
# Symbolic check ran and found no issues, but numerical check failed to run.
# This is a less critical situation, could be a warning or pass.
# For strictness, we can indicate numerical part failed.
print(f"Warning: Symbolic differentiability check passed for '{func_str}', but the "
f"numerical check failed to execute: {e_numerical_setup}. Proceeding with caution.")
# If we reached here and problem_points_reports (from symbolic) has content, means symbolic found issues
# but numerical check ran clean (or also failed to run but symbolic was primary).
if problem_points_reports and not numerical_issue_points: # Symbolic found issues, numerical ran and was clean
raise ValueError(
f"Function '{func_str}' may not be differentiable across domain {domain_tuple}. "
f"Symbolic derivative '{sp.latex(raw_derivative_expr_check)}' has undefined/"
f"discontinuous points at/within: {', '.join(problem_points_reports)}. "
f"Numerical check did not find additional issues (or did not complete)."
)
# If both checks ran and found nothing, the function is considered differentiable for the script's purposes.
class SkibidiToilet(ThreeDScene):
# division by 0 safety functions
@staticmethod
def safeNorm(vec: np.ndarray) -> float:
"""
Compute ‖vec‖, but never return less than eps.
"""
n = np.linalg.norm(vec)
return n if n >= 1e-6 else 1e-6
# scales y axis to (1, 2, or 5) * 10^x with between 3-10 ticks
@staticmethod
def yCfg() -> list:
"""
Determines the y-axis configuration for plotting based on the range of f(x).
• Computes the maximum absolute value of f over DOMAIN
• Chooses a step size from (1, 2, 5) * 10^exp to get between MINTICKS and MAXTICKS
• Returns [y_min, y_max, y_step]
"""
# give me a list of evenly spaced values
# [DOMAIN[0], DOMAIN[1]]
yRange = float(np.max(np.abs(f(np.linspace(*DOMAIN, TICKRANGEACC)))))
# log10 of the max, so it's 1 2 or 5 *10^exp is the max
exp = int(np.floor(np.log10(yRange)))
# holds ts
candidates: list[int] = []
for offset in (-1, 0, 1):
for base in (1, 2, 5):
candidates.append(base * 10 ** (exp + offset))
# try small one first
candidates.sort()
# check to see if some of them work
for step in candidates:
if MINTICKS <= (yRange / step) <= MAXTICKS:
yStepSize = step
break
# if no work just choose closest to middle
else:
yStepSize = min(
candidates,
key = lambda s: abs((yRange / s) - ((MAXTICKS - MINTICKS) / 2))
)
# 0.1 is how much padding to use
return [-(0.1 + 1) * yRange, (0.1 + 1) * yRange, yStepSize]
# returns perpendicular vector
@staticmethod
def perpline(vec: np.ndarray) -> np.ndarray:
"""
• Takes an input array [x, y, 0]
• Returns [-y, x, 0] normalized to unit length
"""
# rotate ts 90°
perp = np.array([-vec[1], vec[0], 0.0])
# find length
# use safeNorm for /0 safety
return perp / SkibidiToilet.safeNorm(perp)
# smooth runtime total time with factor being slowest to fastest
@staticmethod
def runtime(total: float, factor: float, loopCtr: int, maxItr: int) -> float:
"""
Calculates per-iteration runtimes that sum to `total`, following an inverted-gaussian curve
whose slowest duration is `factor` times the fastest.
• total: total time for all iterations combined
• factor: slowest_duration / fastest_duration
• loopCtr: current iteration index (0-based)
• maxItr: total number of iterations
The runtime for iteration i is:
r_min * (1 + (factor - 1) * weight_i)
where weight_i = 1 - exp(-x_i^2) for x_i mapped linearly in [-1.5,1.5],
and r_min is chosen so that sum(runtime_i over i) == total.
"""
# early exit
if maxItr <= 0:
raise ValueError("maxItr must be positive")
# weights for all iterations
weights = []
for i in range(maxItr):
t = i / (maxItr - 1) if maxItr > 1 else 0.0
x = -1.5 + 3.0 * t
weights.append(1.0 - np.exp(-x * x))
W = sum(weights)
# total sum S = sum[r_min * (1 + (factor-1)*w_i)] = r_min * [N + (factor-1)*W]
N = maxItr
denom = N + (factor - 1) * W
r_min = total / denom
# now compute this iteration’s runtime
w_i = weights[loopCtr]
return r_min * (1.0 + (factor - 1) * w_i)
def generate_fprime_line(self: 'SkibidiToilet') -> Line:
current_x = self.fptracker.get_value()
current_y = f(current_x)
# Calculate the y-component of the tangent vector using fprime(x)
# fprime is your derivative function, already defined
tangent_y_component = fprime(current_x)
# Handle cases where fprime might return non-finite values (inf, nan)
# Although your safeDiv should prevent np.inf, this is good for robustness
if not np.isfinite(tangent_y_component):
# If derivative is inf (e.g. vertical tangent), use a large number.
# The sign might matter depending on the function. For sqrt(x) at x->0+, it's positive.
tangent_y_component = 1e9 # A large finite number for a very steep slope
# Raw tangent direction vector in world coordinates: [dx, dy, dz]
# We assume dz=0 for a 2D function graph in the xy-plane.
# dx is taken as 1 unit in world coordinates.
direction_vector_world = np.array([1.0, tangent_y_component, 0.0])
# Normalize the direction vector and scale it by TANGENTLEN
# safeNorm ensures we don't divide by zero if direction_vector_world is tiny
# (not an issue here since x-component is 1.0, so norm is at least 1.0)
norm_of_direction = self.safeNorm(direction_vector_world)
scaled_direction_vector = (direction_vector_world / norm_of_direction) * TANGENTLEN
# Calculate start and end points of the tangent line segment in world coordinates
# The segment is centered at (current_x, current_y)
start_x_world = current_x - scaled_direction_vector[0] / 2.0 # Divide by 2 if TANGENTLEN is total length
y_start_world = current_y - scaled_direction_vector[1] / 2.0
end_x_world = current_x + scaled_direction_vector[0] / 2.0
y_end_world = current_y + scaled_direction_vector[1] / 2.0
start_point_world = np.array([current_x - scaled_direction_vector[0],
current_y - scaled_direction_vector[1], 0.0])
end_point_world = np.array([current_x + scaled_direction_vector[0],
current_y + scaled_direction_vector[1], 0.0])
# Convert world coordinates to Manim's scene coordinates using axes.c2p
start_point_scene = self.axes.c2p(start_point_world[0], start_point_world[1], start_point_world[2])
end_point_scene = self.axes.c2p(end_point_world[0], end_point_world[1], end_point_world[2])
return Line(start_point_scene, end_point_scene, color=FPRIMECOLOR, stroke_width=STROKEWIDTH)
# add background music
@staticmethod
def addMusic(scene: Scene, track: str = MUSICPATH) -> AudioSegment:
"""
Post-processes the rendered scene:
• trims `track` to the exact scene duration
• applies a linear fade-out over the last FADEOUTTIME seconds
• injects the audio at t = 0 into the final video
"""
# scene ms means ms
sceneMs = int(scene.duration * 1000)
audio = (AudioSegment.from_file(track)
# boost it the entire time
.apply_gain(GAIN[0])
# trim it to the length of the scene
[:sceneMs]
# the fade out thing
.fade_out(min(sceneMs, FADEOUTTIME * 1000)))
# add ts to video
scene.renderer.file_writer.add_audio_segment(audio, 0)
# color to hex
@staticmethod
def asHex(col: ManimColor) -> str:
"""
Converts a Manim Color object to a hexadecimal color string.
• Uses the Color.to_hex() method and strips the '#' prefix
"""
return col.to_hex()[1:]
def construct(self: 'SkibidiToilet') -> None:
# fail if function sucks
check_function_differentiability(FUNCTION, DOMAIN, XSEGSIZE, TOLERANCE)
# lists
dots: list[Dot] = []
chords: list[Line] = []
frusta: list[Surface] = []
bounds: list[Dot] = []
arcLengthStr: list[str] = []
arcLengths: list[MathTex] = []
onScreen: list[MathTex] = []
positioned_mobjects_list: list[MathTex] = []
surface_area_plug_latex_strings: list[str] = []
formulas_to_animate: list[MathTex] = []
# color palllete thing
tpl = TexTemplate()
tpl.add_to_preamble(r"\usepackage{xcolor}")
colorMap = {
"SACOLOR": SACOLOR,
"BOUND_A": BOUNDCOLORS[0],
"BOUND_B": BOUNDCOLORS[1],
"fxcolor": GRAPHCOLOR,
"fprimecolor": FPRIMECOLOR,
"dxcolor": DXCOLOR,
"dycolor": DYCOLOR,
"dscolor": DSCOLOR,
"frustumcolor": FRUSTUMCOLOR
}
for name, col in colorMap.items():
tpl.add_to_preamble(
rf"\definecolor{{{name}}}{{HTML}}{{{SkibidiToilet.asHex(col)}}}"
)
# bunch of functions
# arc length ladder
# pythag thm
arcLengthStr.append(
r"\textcolor{dscolor}{ds}^{2}"
r" = \textcolor{dxcolor}{dx}^{2}"
r" + \textcolor{dycolor}{dy}^{2}"
)
# sqrt it
arcLengthStr.append(
r"\textcolor{dscolor}{ds}"
r" = \sqrt{\textcolor{dxcolor}{dx}^{2}"
r" + \textcolor{dycolor}{dy}^{2}}"
)
# dy -> (dy/dx) * dx
arcLengthStr.append(
r"\textcolor{dscolor}{ds}"
r" = \sqrt{\textcolor{dxcolor}{dx}^{2}"
r" + \left(\frac{\textcolor{dycolor}{dy}}{\textcolor{dxcolor}{dx}}"
r"\,\textcolor{dxcolor}{dx}\right)^{2}}"
)
# dy/dx -> f'(x)
arcLengthStr.append(
r"\textcolor{dscolor}{ds}"
r" = \sqrt{\textcolor{dxcolor}{dx}^{2}"
r" + \left(\textcolor{fprimecolor}{f'(x)}\,\textcolor{dxcolor}{dx}\right)^{2}}"
)
# factor out dx^2
arcLengthStr.append(
r"\textcolor{dscolor}{ds}"
r" = \sqrt{\textcolor{dxcolor}{dx}^{2}"
r"\!\left(1 + \left(\textcolor{fprimecolor}{f'(x)}\right)^{2}\right)}"
)
# final
arcLengthStr.append(
r"\textcolor{dscolor}{ds}"
r" = \sqrt{1 + \left(\textcolor{fprimecolor}{f'(x)}\right)^{2}}"
r"\,\textcolor{dxcolor}{dx}"
)
formula = MathTex(
r"\textcolor{fxcolor}{f(x)} = " + func_latex,
font_size = FONTSIZE,
tex_template = tpl,
).to_edge(DL)
# inside construct, after building tpl and colorMap…
fprimeFormula = MathTex(
r"\textcolor{fprimecolor}{f'(x)} = " + diff_latex,
font_size = FONTSIZE,
tex_template = tpl,
).to_corner(DR, buff = 0.5)
# circ * ds
surfaceAreaCirc = MathTex(
r"\textcolor{frustumcolor}{Frustum} = \text{circumference}\,*\textcolor{dscolor}{ds}",
font_size = FONTSIZE,
tex_template = tpl,
).to_edge(UR)
# 2πr * ds
surfaceAreaTrans = MathTex(
r"\textcolor{frustumcolor}{Frustum} = 2\pi\,\textcolor{fxcolor}{f(x)}*"
r"\textcolor{dscolor}{ds}",
font_size = FONTSIZE,
tex_template = tpl,
).to_edge(UR)
# differential surface area
surfaceArea = MathTex(
r"\textcolor{frustumcolor}{Frustum} = 2\pi\,\textcolor{fxcolor}{f(x)}\,"
r"\sqrt{1 + \left(\textcolor{fprimecolor}{f'(x)}\right)^{2}}"
r"\,\textcolor{dxcolor}{dx}",
font_size = FONTSIZE,
tex_template = tpl,
).to_edge(UR)
# integral surface area
integralLatex = (
r"\textcolor{SACOLOR}{\text{Surface Area}}"
r"\;=\;"
r"\int_{\textcolor{BOUND_A}{a}}^{\textcolor{BOUND_B}{b}}"
r" 2\pi\,\textcolor{fxcolor}{f(x)}"
r"\,\sqrt{1 + \bigl(\textcolor{fprimecolor}{f'(x)}\bigr)^{2}}"
r"\,\textcolor{dxcolor}{dx}"
)
surfaceAreaInt = MathTex(
integralLatex,
font_size = FONTSIZE,
tex_template = tpl,
).to_edge(UR)
# draw f(x) in 3d
self.axes = ThreeDAxes(
x_range = [0, DOMAIN[1], XTICKSTEP],
y_range = [*SkibidiToilet.yCfg()],
z_range = [*SkibidiToilet.yCfg()],
x_length = XGRAPHSIZE,
y_length = YGRAPHSIZE,
z_length = YGRAPHSIZE,
).to_edge(LEFT, DOWN)
# the graph
funcGraph = self.axes.plot(f, x_range = [*DOMAIN], color = GRAPHCOLOR)
# final surface Area
surface = Surface(
lambda u, v: self.axes.c2p(
u,
f(u) * np.cos(v),
f(u) * np.sin(v),
),
u_range = DOMAIN,
v_range = [0, TAU],
resolution = NUMFACES,
)
# add some stuff to the surface area
surface.set_style(
fill_color = SACOLOR,
fill_opacity = OPACITY,
stroke_width = 0,
)
# make the dots
steps = int(math.ceil((DOMAIN[1] - DOMAIN[0]) / XSEGSIZE))
for i in range(steps + 1): # Loop from 0 to steps inclusive
if i < steps:
# For intermediate points, calculate x as usual
x = DOMAIN[0] + i * XSEGSIZE
else:
# For the very last point (i == steps), force x to be DOMAIN[1]
# This ensures the graph covers the exact domain and avoids small
# floating point overshoots or misalignments.
x = DOMAIN[1]
dots.append(Dot(self.axes.c2p(x, f(x), 0), color = DOTCOLOR))
# differential lines
dx = (Line(
self.axes.c2p(DOMAIN[0], f(DOMAIN[0]), 0),
self.axes.c2p(DOMAIN[0] + XSEGSIZE, f(DOMAIN[0]), 0),
color = DXCOLOR
))
dy = (Line(
self.axes.c2p(DOMAIN[0] + XSEGSIZE, f(DOMAIN[0] + XSEGSIZE), 0),
self.axes.c2p(DOMAIN[0] + XSEGSIZE, f(DOMAIN[0]), 0),
color = DYCOLOR
))
ds = (Line(
self.axes.c2p(DOMAIN[0], f(DOMAIN[0]), 0),
self.axes.c2p(DOMAIN[0] + XSEGSIZE, f(DOMAIN[0] + XSEGSIZE), 0),
color = DSCOLOR
))
# differential labels
dsVec = ds.get_end() - ds.get_start()
# cuz [x, y, z] so 1 is y and the sign thing
slopeSign = np.sign(dsVec[1])
dxDirection = UP
if slopeSign > 0:
dxDirection = DOWN
dyDirection = RIGHT
dsDirection = -self.perpline(dsVec)
if slopeSign > 0:
dsDirection = self.perpline(dsVec)
dxBrace = BraceBetweenPoints(
dx.get_start(),
dx.get_end(),
direction = dxDirection,
stroke_width = STROKEWIDTH,
color = DXCOLOR
)
dyBrace = BraceBetweenPoints(
dy.get_start(),
dy.get_end(),
direction = dyDirection,
stroke_width = STROKEWIDTH,
color = DYCOLOR
)
dsBrace = BraceBetweenPoints(
ds.get_start(),
ds.get_end(),
direction = dsDirection,
stroke_width = STROKEWIDTH,
color = DSCOLOR
)
'''
dxBrace.set_fill(opacity=0) # no interior fill
dxBrace.set_stroke(width=STROKEWIDTH)
dyBrace.set_fill(opacity=0) # no interior fill
dyBrace.set_stroke(width=STROKEWIDTH)
dsBrace.set_fill(opacity=0) # no interior fill
dsBrace.set_stroke(width=STROKEWIDTH)
'''
dxLabel = MathTex(
r"dx",
font_size = LABELSIZE,
color = DXCOLOR
).next_to(dxBrace, dxDirection, buff = 0.1)
dyLabel = MathTex(
r"dy",
font_size = LABELSIZE,
color = DYCOLOR
).next_to(dyBrace, dyDirection, buff = 0.1)
dsLabel = MathTex(
r"ds",
font_size = LABELSIZE,
color = DSCOLOR
).next_to(
dsBrace,
dsDirection,
# its not close enough vro
buff = 0
)
# f'(x)
# direction of ds to direction of the tangent
slopeDir = dsVec / self.safeNorm(dsVec)
anchorPt = ds.get_start() - slopeDir * FPRIMELABELOFFSET
# 1) a tracker for the x-position along [a,b]
self.fptracker = ValueTracker(DOMAIN[0])
# 2) an always_redraw Line that recomputes slope & endpoints each frame
fprimeLine = always_redraw(self.generate_fprime_line) # Pass THE BOUND METHOD
# base for each frusta
chords.append(ds)
for i in range(1, steps):
a = DOMAIN[0] + i * XSEGSIZE
b = DOMAIN[0] + (i + 1) * XSEGSIZE
if b > DOMAIN[1]: b = DOMAIN[1]
segColor = DSCOLOR
if i > 0:
segColor = CHORDCOLORS[(i - 1) % len(CHORDCOLORS)]
chords.append(Line(
self.axes.c2p(a, f(a), 0),
self.axes.c2p(b, f(b), 0),
color = segColor
))
# rotate and spit on that thang
for i in range(steps):
# u-range for this slice
a = DOMAIN[0] + i * XSEGSIZE
b = min(DOMAIN[1], a + XSEGSIZE)
# pick slice color
segColor = FRUSTUMCOLOR
if i > 0:
segColor = CHORDCOLORS[(i - 1) % len(CHORDCOLORS)]
# endpoints of the chord
rad_a, rad_b = f(a), f(b)
slice_surface = Surface(
lambda u, v: self.axes.c2p(
u,
(rad_a + (rad_b - rad_a) * safeDiv(u - a, b - a)) * np.cos(v),
(rad_a + (rad_b - rad_a) * safeDiv(u - a, b - a)) * np.sin(v),
),
u_range = [a, b],
v_range = [0, TAU],
resolution = [1, NUMFACES[1]]
).set_style(
fill_color = segColor,
fill_opacity = OPACITY,
stroke_width = 0,
)
frusta.append(slice_surface)
# integral bounds for color dots at the end
bounds.append(
Dot(self.axes.c2p(DOMAIN[0], f(DOMAIN[0]), 0), color = BOUNDCOLORS[0])
)
bounds.append(
Dot(self.axes.c2p(DOMAIN[1], f(DOMAIN[1]), 0), color = BOUNDCOLORS[1])
)
# committing crimes with both direction and magnitude
totalFrusta = VGroup(*frusta)
graph = VGroup(self.axes, funcGraph)
diffChords = VGroup(dx, dy, ds, fprimeLine)
diffBraces = VGroup(dxBrace, dyBrace, dsBrace)
diffLabels = VGroup(dxLabel, dyLabel, dsLabel)
dxLabelC = dxLabel.copy()
dyLabelC = dyLabel.copy()
dsLabelC = dsLabel.copy()
dxBraceC = dxBrace.copy()
dyBraceC = dyBrace.copy()
dsBraceC = dsBrace.copy()
diffBracesCopy = VGroup(dxBraceC, dyBraceC, dsBraceC)
diffLabelsCopy = VGroup(dxLabelC, dyLabelC, dsLabelC)
diffCopy = VGroup(diffLabelsCopy, diffBracesCopy)
differentials = VGroup(diffChords, diffBraces, diffLabels)
# bermuda triangle
triangle_assembly = VGroup(dx, dy, ds, dxBrace, dyBrace, dsBrace, dxLabel, dyLabel, dsLabel)
# auto zoom and get dimensions
ta_width = triangle_assembly.width
ta_height = triangle_assembly.height
# what is the screen space going to use thing
# percent of overall width or height
target_tri_screen_width_frac = 0.42
target_tri_screen_height_frac = 0.80
# screen dimensions for the triangle thing
target_tri_effective_screen_width = XGRAPHSIZE * target_tri_screen_width_frac
target_tri_effective_screen_height = YGRAPHSIZE * target_tri_screen_height_frac
# dont divide by 0
# but if its too small dont zoom in
if ta_width < TOLERANCE or ta_height < TOLERANCE:
auto_zoom = 1.0
# idk what this is for i forgot
else:
zoom_needed_for_width = target_tri_effective_screen_width / ta_width
zoom_needed_for_height = target_tri_effective_screen_height / ta_height
auto_zoom = min(zoom_needed_for_width, zoom_needed_for_height)
# calc center zoom placement
vis_world_w = XGRAPHSIZE / auto_zoom
# what
actual_ta_center_x = triangle_assembly.get_center()[0]
actual_ta_center_y = triangle_assembly.get_center()[1]
# make sure its on the left side
# but i forgot hwo ti works
cam_cx = actual_ta_center_x - \
(target_tri_effective_screen_width / (2 * auto_zoom)) + (vis_world_w / 2)
# center thing veritcal
cam_cy = actual_ta_center_y
pythagSceneCenter = np.array([cam_cx, cam_cy, 0.0])
# panel dimensions
# h and w frac percent something
panel_screen_width_frac = 0.6
panel_screen_height_frac = 0.90
# ZOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOM
panel_world_width = (XGRAPHSIZE * panel_screen_width_frac) / auto_zoom
panel_world_height = (YGRAPHSIZE * panel_screen_height_frac) / auto_zoom
# draw erct
panel = Rectangle(
width=panel_world_width,
height=panel_world_height,
fill_color=BLACK,
fill_opacity=0.8,
stroke_opacity=0
)
# padding
# why did i add this
panel_padding_from_screen_right_frac = 0.02
panel_padding_from_screen_right_world = \
(XGRAPHSIZE * panel_padding_from_screen_right_frac) / auto_zoom
# panel cener x
panel_center_x = (pythagSceneCenter[0] + vis_world_w / 2) - \
panel_padding_from_screen_right_world - (panel_world_width / 2)
# vert align panel
panel_center_y = pythagSceneCenter[1]
panel.move_to(np.array([panel_center_x, panel_center_y, 0.0]))
# formula space it
# idk what the number means but it affects spacing
formulas_block_height_frac = 0.9
block_height = panel_world_height * formulas_block_height_frac
if MAXONSCREEN <= 0: raise ValueError("MAXONSCREEN must be positive")
if MAXONSCREEN == 1:
dynamic_spacing = block_height
else:
dynamic_spacing = block_height / (MAXONSCREEN - 1)
formula_padding_from_panel_left_frac = 0.08
formula_x_align_left = panel.get_left()[0] + \
panel_world_width * formula_padding_from_panel_left_frac
# wtf
final_top_y_for_formulas = panel.get_center()[1] + dynamic_spacing * (MAXONSCREEN - 1) / 2.0
# re position the thing
for i in range(len(arcLengths)):
arcLengths[i].move_to(
(
formula_x_align_left,
final_top_y_for_formulas - i * dynamic_spacing,
0
),
aligned_edge = LEFT
)
# oh yeah
# vector
equations_group = VGroup(*arcLengths)
# at the start graph f(x)
self.move_camera(**NORMANGLE)
self.wait(WAITCAMERA)
# maybe get rid of the z axis?
self.play(Create(self.axes), run_time = RUNTIME)
self.play(Create(funcGraph), run_time = RUNTIME)
# formulas gang
self.play(Write(formula), run_time = RUNTIME)