Portfolio website logo featuring Charm, a black cat with yellow eyes, and the letters 'dmg' above his head
Charm's Jobs website logo featuring Charm, a black cat with yellow eyes and a confident smile
You Can Verify
This site is my portfolio. It and are my first web projects.
The repository
is available on GitHub.
Originally, my plan was to create a facsimile of my old resume
and publish it to the web. To accomplish that plan, I started teaching myself HTML, CSS, and JavaScript. MDN Web Docs
, an invaluable web developer wiki, soon became my primary learning resource.
Fast-forward several months later and I have a resume, a portfolio with two projects, and logos based on my cats — and they talk.
The Charm (black cat) and Shelby (brown cat) logos are based on artwork by Charlotte Ma. I drew them with an SVG editing program, Boxy SVG
. All the other SVG artwork, including the seasonal logo artwork, associated animations, and paw cursors, is my own work.
The fonts that I use, Open Sans
and Comic Neue
, are by Steve Matteson and Craig Rozynski, respectively. The icons, except for the sun/moon icon, are Material Icons
by Google.
One feature that I built depended on solving cubic parametric equations (cubic Bézier curves). Solving these equations is something your browser does every time it renders CSS Animations or Web Animations, and I needed the same functionality for my smooth scroller implementation. Instead of implementing my own root-finding algorithms to solve the equations, I decided to port WebKit's algorithm
Everything else — the design, the scrolling systems, the type and talk system, the interactive elements, the logic, everything — all down to the smallest detail, was built with JavaScript, HTML, and CSS. No libraries. No frameworks.
Momentum Scrolling System — This momentum scrolling system for non-touch-screen devices simulates the touch-based scrolling systems found on smartphones and tablets.
This feature is intended to be used by mouse-users since mouse drivers typically do not include momentum-scrolling features. It may conflict with trackpads that provide momentum-scrolling features.
A momentum scrolling system for mouse-users
The way it works is familiar: faster flicks produce more momentum, resulting in scrolls that travel farther and last longer. Slower flicks have the opposite effect. Flicking in the same direction in quick succession leads to momentum accumulation, i.e. faster scrolling.
When the border of a scroll container is hit by a momentum scroll, it responds by bouncing. Harder impacts produce larger bounces.
If a user tries to scroll beyond a border, the scroll container will stretch in the direction of the pointer while the pointer is being held down, but will then rebound when the pointer is let go.
One-dimensional and two-dimensional scroll containers are supported and both axes may be scrolled simultaneously.
The system depends on a JavaScript module that I built, called MomentumScroller
. It has its own repository and documentation. Briefly, however, instantiation of a MomentumScroller simply requires a reference to the scroll container. Optionally, you may chain various setters, either during or after instantiation, to set custom grab and grabbing cursors, set the deceleration rate, and more.
After instantiation, pointer flicks on the scroll container will generate momentum scrolls.
Below is a demonstration of a momentum scroller container along with controls for adjusting the number of axes, the deceleration level, and the bounciness level.
Note that this demonstration will be disabled on touch-screen devices since they provide their own native momentum scrolling systems.
The Yowl, by Charlotte Ma, is used as the background image of the two-dimensional scroll container in this demonstration.
Momentum Scroller Demo
Momentum scrolling is disabled
Elapsed Time
The generation of momentum scrolls is simply an application of kinematic equations
. After deceleration is set, the only missing variable needed to calculate scroll duration, distance, and direction is pointer velocity. The momentum accumulation feature is accomplished by adjusting a velocity multiplier up or down depending on the direction and timing criteria between scrolls.
Scrolling will stop if scroll duration has elapsed, or if one of the two edges of a one-dimensional scroller has been reached, or if one of the four vertices of a two-dimensional scroller has been reached, or if there is a "pointerdown" event on the scroll container.
Notably, MomentumScroller is a system that works well with other scrollers and elements that depend on "pointerdown" events. By default, it works well with commonly used elements such as anchors, buttons, and text boxes, but it can easily be customized to work with any element that it needs to share "pointerdown" events with.
For example, there are interactive elements on this site, such as links and video galleries, that are descendants of the main momentum scroller and which also need to respond to pointer events. Logic was created to resolve these pointer event conflicts so that the user experience is intuitive. Nested MomentumScrollers also work perfectly well together.
The momentum scrolling system is enabled by default on non-touch-screen devices, but it may be disabled if desired. The preferred setting is remembered for future visits.
Advanced Smooth Scrolling — This smooth scrolling implementation provides advanced, easy-to-use smooth scrolling functionality. It allows you to customize smooth scrolling properties, such as duration and easing. Additionally, it includes advanced features such as scroll events and promises.
Every smooth scroll on this site is accomplished with this implementation.
One-dimensional and two-dimensional scroll containers are supported and both axes may be scrolled simultaneously.
This implementation depends on a JavaScript module that I built, called SmoothScroller
. It has its own repository and documentation. Briefly, however, smooth scrolling may be accomplished by calling the static SmoothScroller.scroll() method or by calling an instance's scroll() method.
Below is a demonstration smooth scroller container along with controls for adjusting the easing, duration, x-position, and y-position.
Although the easing selector only includes five options, the scroll method's easing argument accepts any valid set of control points (e.g. [ P1x, P1y, P2x, P2y ]), along with the standard keywords you may use when writing CSS transitions and animations (e.g. "ease", "ease-in", "linear", etc.).
The results of the returned promise will appear once the scroll has completed.
Note that if momentum scrolling is enabled, this smooth scroller container will simultaneously function as a momentum scroller container, allowing you to test the interactions between the different scroller types.
The image used as the background in this demonstration is The Scream
by Edvard Munch.
Smooth Scroller Demo
Start Scrolling
Elapsed Time
Whereas a momentum scroll's position over time is calculated by solving kinematic equations, a smooth scroll's position over time is calculated by solving cubic parametric equations.
This is where WebKit's cubic Bézier root-finding algorithm comes in.
I supply the algorithm with control points and a time ratio and it returns a scroll ratio, which I then convert to a scroll position.
I created this implementation because the standard method for producing a smooth scroll
  behavior: "smooth",
  top: 200,
produces inconsistent results across browsers in terms of duration and easing, is not supported by Safari
, and — most importantly — does not tell you if the scroll made it to the intended destination (e.g. top: 200 in the above example).
Unlike an instant scroll, a smooth scroll can be interrupted. You cannot assume that the scroll made it. Some parts of my code depend on knowing the outcome of a smooth scroll, and this information is provided by the promise that is returned.
Intersection Observers
, which I use on this site, may also work to solve this problem, but I prefer that the solution be built into the scrolling method itself.
Responsive Video Galleries — All video galleries are aware of their location relative to the viewport, as well as their location relative to other video galleries, and they use this data to intelligently decide when to play, stop, or scroll content.
Section Scroller — At the top of the portfolio is the section scroller. It emerges from the slogan during the loading animation sequence, and it is used for quickly navigating the portfolio. It also automatically scrolls itself when necessary to keep in sync with the main content.
Original Logos — Based on my two kittehs, Charm and Shelby, these original logos blink and talk when you tap them. Festive, seasonal logo themes automatically appear during certain times of the year.
The current kitteh is and the current theme is . Using the selectors below, you can switch between Charm and Shelby and the various themes.
Kitteh Logo and Theme Demo
I also created SVGs in the shape of Charm's and Shelby's paws that serve as the grab and grabbing cursors while the momentum scrolling system and selectors are in use.
Charm's and Shelby's paws serve as the grab and grabbing cursors
Original Opening Animation Sequence — The logo, slogan, section scroller, and other elements have been carefully choreographed to create a delightful opening animation sequence.
An original opening animation sequence
Other animations, such as the ripple effect on buttons and links, and the transition between the sun and moon icons, are original code, but were inspired by other designs.
Type and Talk System — This typewriter-style messaging system hearkens to the old-school dialogue systems of 80s and 90s video games.
Messages are typed, character-by-character, inside a bubble below the Charm or Shelby logo. If an audio source is provided, it will play as the message is typed. The syntax is:
TypeAndTalk.submitMessage(message, options)
The message parameter expects a string, which may also include commands within the string. The {blink} and {longblink} commands make the Charm or Shelby logo do a normal or long blink, respectively. The {0–5000} command delays typing for the next 0–5000 milliseconds.
The options parameter, if supplied, allows setting the audioSource, delayStart, delayEnd, and delayBetweenChars properties.
If an audioSource is set, it will play while the message is typing. However, the audio will be muted until the user unmutes the sound by tapping the sound button inside the message bubble.
The delayStart and delayEnd options allow you to delay the beginning of typing and the clearing away of the message, respectively, by a number of milliseconds. The arguments must be a number between 0 and 5000. Numbers outside this range will be adjusted.
The delayBetweenChars option expects a number between 0 and 120, which sets the number of milliseconds delay between typed characters.
Message Typing Demo
Message typing is paused while menu is open
Submit Message
Longer delays are automatically added after punctuation, unless overridden with an intrastring delay command, and a message is automatically cleared from the screen shortly after typing has finished, unless another message replaces it sooner.
If the “···” menu is opened while a message is on screen, the message will fade out, and typing and audio will pause. The message will fade in and typing and audio will resume once the “···” menu is closed.
A message's status (untyped, typing, typing (paused), typed) is updated as necessary, and the promise returned by the submitMessage method resolves once the message has finished typing.
Want to speed up the typing? You can! Just tap and hold the message. This is a behavior commonly implemented in video games with lots of dialogue.
Dark and Light Modes — On first visit, the site's appearance is determined by the device's mode, light or dark, if available. If not, the appearance defaults to light. The appearance may be toggled between light and dark, and the preferred setting is remembered for future visits.
Keyboard Accessibility — All interactive elements are keyboard accessible. Simply tab to an element and press the "Enter" key to interact with it.
Elements intuitively gain or lose focus when necessary depending on site circumstances.
Cancellable Actions — If the "Escape" key is pressed while holding the "Enter" key or while holding the pointer on an interactive element, the behavior that would have been triggered is canceled.
Additionally, if you tap and hold on an interactive element, and, while holding, move the pointer outside of the target and let go, the action will be canceled.
Except for selectors, actions are only executed if the "pointerdown" and "pointerup" targets match. Selectors are cancelled only if, while the selector is being held, the "Escape" key is pressed or the pointer goes beyond the edge of a touch-screen.
Charm's Jobs Sardine theme background varies with the time of day
Watch the full video demo of the Charm's Jobs app
This is Charm's Jobs. It and are my first web projects.
The Charm's Jobs app is a progressive web app. It will be the home for the upcoming book series “Charm's Jobs.”
As with my portfolio, everything in Charm's Jobs is made from scratch with basic tools — JavaScript, HTML, and CSS. No libraries. No frameworks.
The Charm's Jobs logo and the artwork for the Sardine theme are by Charlotte Ma. The font, Comic Neue
, is by Craig Rozynski, and the icons, Material Icons
, are by Google.
Charm's Jobs is fully compatible with the Blink rendering engine. Compatibility with the Gecko rendering engine is nearly 100%, but because Gecko does not support CSS backdrop-filter
, parts of the Sardine theme do not look as intended. Work on WebKit compatibility is ongoing.
Responsive Design — The book displays one or two pages depending on the book container's aspect ratio — not simply the device's orientation.
Although the book container's aspect ratio is influenced by the device's orientation, it is also influenced by the height of the bottom navigation bar, which may be in a raised or lowered position.
Works Offline — Books can be read offline as long as they have been downloaded. The Charm's Jobs app leverages the service worker API and cache storage API, along with a custom file versioning system, to efficiently manage cache.
Every time a book is loaded, the app checks that all required resources are downloaded and current. Outdated resources are automatically purged. Just one HTTP request is required to check the freshness of all resources.
Touch Gestures and Keyboard Shortcuts — Navigating Charm's Jobs is intuitive. Custom touch gestures and keyboard shortcuts are available to turn pages, control audio playback, and adjust volume.
Saved Preferences — User preferences, such as the last-used theme (including time of day and weather for the Sardine theme), volume level, auto-play, and bottom navigation bar position, are saved for future visits.
Auto Resume — The last page that a user viewed is remembered so that the book will automatically scroll to that page the next time the book is opened.
Real Progress Bars — Progress bars are shown when book resources are being loaded or downloaded and they reflect the actual progress of the data being downloaded to the device.
Sardine Theme Features
Dynamic Backgrounds — When the time of day is set to current, the background will become a dynamic one, updating itself every minute. When a specific time of day — dawn, sunrise, morning, afternoon, sunset, dusk, or night — is chosen, the background will become static after transitioning to that time of day.
Twinkling stars and a cat-themed-constellation start to appear around sunset. They reach their greatest visibility at night, and then gradually fade away as sunrise approaches.
Weather — When the weather is set to calm, the sky becomes clear and the waves gently sway back and forth. When set to storm, the sky gradually darkens, lightning begins to flash, and the waves become choppier.
Animated Buoy — The brightness of the buoy's lights varies with the time of day. The position of the reflections off the surface of the sea take into account the position of the lights.
The University of Texas at Austin College of Pharmacy
Texas State Board of Pharmacy
American Heart Association
American Pharmacists Association
The University of Texas at Austin College of Pharmacy
American Pharmacists Association
Web Developer and Designer
 — Personal website
 — Audiobook progressive web app
 — JavaScript module that gives mouse-users the ability to scroll through websites with the flick of a mouse
 — JavaScript module that gives developers a powerful and customizable smooth scroll method
Tools — Adobe After Effects, Adobe Illustrator, Adobe Photoshop, Adobe Premiere Pro, Boxy SVG, CSS, Git, GitHub, HTML, JavaScript, JSON, Microsoft Excel, Microsoft IIS, Node.js, PowerShell, Python, SketchUp, SQL, Visual Studio Code
Captioning Agent
Provides excellent customer service by dictating and captioning a variety of conversational topics verbatim
Maintains a high level of dictation speed and accuracy
Maintains strict consumer confidentiality
Pharmacist Intern (Ambulatory Care Rotation)
University Health
Lead patient interviews in a pharmacist-run clinic
Optimized medication regimens in patients with uncontrolled diabetes, hypertension, and dyslipidemia
Promoted behavioral strategies to help patients quit tobacco and recommended tobacco-cessation pharmacotherapy
Pharmacist Intern (Poison Control Rotation)
South Texas Poison Center
Provided initial and follow-up toxicological management consultation to inpatient and outpatient healthcare providers, as well as to the general public
Pharmacist Intern (Internal Medicine Rotation)
Audie L. Murphy Memorial VA Hospital
Optimized patient care by providing daily guideline-based and protocol-directed pharmacy consultation to the internal medicine team
Provided comprehensive counseling to patients being discharged with anticoagulant prescriptions
Critically evaluated an article published in the Annals of Pharmacotherapy and emailed the corresponding author after discovering a major statistical error
Pharmacist Intern (Hospital Pharmacy Rotation)
University Health
Prepared and verified the accuracy of compounded sterile preparations, including IV admixtures and TPNs
Clarified medication orders and provided guideline-based and protocol-directed recommendations to prescribers
Operated Pyxis automated dispensing cabinets
Pharmacist Intern (Pharmacy Management Rotation)
Partnered with Aetna at sales meetings to promote H-E-B pharmacy services to Medicare Part D beneficiaries
Provided MTM services using MirixaPro and OutcomesMTM to H-E-B pharmacy customers
Provided immunization services at two flu clinics
Pharmacist Intern (Psychiatric Pharmacy Rotation)
San Antonio State Hospital
Performed mental status exams and recommended adjustments to antipsychotic medication regimens for patients with severe cases of schizophrenia, bipolar disorder, and other mental illnesses
Assessed for antipsychotic-induced movement disorders and made treatment recommendations to attending psychiatrists
Pharmacist Intern (Community Pharmacy Rotation)
Prevented a potentially significant medication error involving diclofenac gel
Helped a patient identify a medication that was causing intolerable lower extremity edema
Served as the primary immunizer while on duty
Performed prescription transfers, drug utilization review, verification, counseling, annual inventory, recordkeeping, and more, while under preceptor supervision
Pharmacy Technician