pmndrs

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars803
mentions20
016324864Oct 4Oct 18Nov 2
UTC days · repositories pmndrs owns13 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-18

npm207.4M
051.8M103.7M155.5M207.4MOct 4Oct 18Nov 2
Oct 30npm: 2/28 packages observed

Across 28 known packages in repositories pmndrs owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

pmndrs’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside pmndrs’s account where pmndrs opened or reviewed pull requests in the past 365 days.

Who noticed pmndrs

Everyone who starred or forked pmndrs’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

20posts
Nov 30 posts
  1. Building a Luxury 3D Engineering Simulator in WebGL: Next.js 14, Three.js, and Real-Time Physics Building a Luxury 3D Engineering Simulator in WebGL: Next.js 14, Three.js, and Real-Time Physics If you have ever taken a college-level mechanical engineering or physics course, you are intimately familiar with this pain: You are studying Euler–Bernoulli beam flexure, Fourier thermal conduction networks, or crystalline slip planes, and your only visual aids are either: 1. Static 2D textbook cross-sections with cryptic arrows from the 1980s. 2. Clunky, gray Java applets or MATLAB scripts that feel like Windows 95. Governing equations in physics describe continuous spatial fields: curvatures, thermal gradients, atomic packing, and fracture energies. Staring at static formulas like: $$\delta_{max} = \frac{P L^3}{48 E I} \quad \text{or} \quad r_{crit} = \frac{k}{h}$$ often fails to convey the physical intuition behind how structures respond when physical constraints change. I wanted to fix that. I built Mecha Atelier: a luxury visualization studio for mechanical engineering where governing equations become instruments you can touch, tweak, and see in real-time WebGL. Here is a full technical breakdown of how it works under the hood, how we deform 3D meshes along mathematical curves at 60 FPS, and how we tackled state synchronization and WebGL search engine visibility. --- 🛠️ The Tech Stack - Framework: Next.js 14.2 (App Router, Server Components & Edge Runtime) - Language: TypeScript 5 (Strict physical unit models & type safety) - 3D Graphics Engine: Three.js r172 - React 3D Layer: @react-three/fiber (R3F) & @react-three/drei - State Management: Zustand 5 (Fine-grained reactive subscriptions) - Styling: Tailwind CSS + Custom CAD/SolidWorks-inspired luxury palette - Testing: Vitest (Analytical equation verification) + Playwright (WebGL smoke tests) --- 🏛️ Architecture: Decoupling Physics Solvers from the 3D Render Loop A major pitfall in building interactive WebGL apps in React is triggering component re-renders when high-frequency user interactions occur (e.g. dragging a load slider or scrubbing a probe). ┌────────────────────────────────────────┐ │ User Interaction │ │ (Sliders, Keyboards, Camera Orbit) │ └───────────────────┬────────────────────┘ │ ▼ ┌───────────────────────────┐ │ Zustand Store │ │ (Decoupled State Slices) │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ Analytical Math │ │ 3D Stage (R3F) │ │ Solvers (<1 ms) │ │ Direct Buffer │ │ Pure TypeScript │ │ Vertex Updates │ └────────┬─────────┘ └────────┬─────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌──────────────────┐ │ UI Telemetry & │ │ Locked 60 FPS │ │ Real-Time Badges │ │ WebGL Viewport │ └──────────────────┘ └──────────────────┘ 1. State Isolation: User inputs are dispatched directly to dedicated Zustand stores (useBeamStore, useThermalStore, useMaterialStore). 2. Deterministic Analytical Solvers: All physical computations reside in pure utility functions in utils/. They execute synchronously in <1 ms. 3. Subscribed Viewports: The 3D canvases subscribe only to their specific state slices, preventing canvas rebuilds. --- 📐 Deep Dive 1: Real-Time Euler–Bernoulli Beam Mesh Deformation In the Mechanics of Materials (MOM-01) stage, users analyze beams under point loads, distributed loads (UDL), and cantilever constraints. Traditional finite element analysis (FEA) solvers take seconds to mesh and solve. To achieve an instantaneous 60 FPS response, we evaluate the exact closed-form Euler–Bernoulli deflection equations and dynamically displace vertices directly within the GPU's buffer attributes. How Vertex Displacement Works: 1. We generate an extruded 2D cross-section profile (IPE standard I-beams, rectangular tubes, or circular solid shafts) along the Z-axis using Three.js ExtrudeGeometry. 2. We rotate the geometry so the beam lies along the global X-axis, centered at the origin. 3. Instead of recreating geometries when the load changes, we directly mutate the vertex height ($Y$) inside the position buffer attribute: // components/three/AppBeam.tsx const geometry = useMemo(() => { const profile = buildProfile(shape, widthWorld, depthWorld, inputs); const geo = new THREE.ExtrudeGeometry(profile, { depth: worldLength, bevelEnabled: false, steps: segments, curveSegments: shape === "circle" ? 32 : 1, }); // Extrusion runs along +Z; rotate so beam lies along +X, then center geo.rotateY(Math.PI / 2); geo.translate(-worldLength / 2, 0, 0); const position = geo.attributes.position; for (let i = 0; i < position.count; i++) { const x = position.getX(i); const t = worldLength > 0 ? (x + worldLength / 2) / worldLength : 0.5; // Displace Y based on exact governing Euler-Bernoulli equation position.setY(i, position.getY(i) + worldDeflectionAt(t)); } position.needsUpdate = true; geo.computeVertexNormals(); // Recalculate normals for proper metallic reflections return geo; }, [shape, worldLength, depthWorld, widthWorld, worldDeflectionAt, segments, inputs]); Pairing this metallic material with Drei’s <Edges threshold={30} color="#33465C" /> achieves the sleek, technical blueprint aesthetic reminiscent of CAD software like SolidWorks or Siemens NX. --- 🌡️ Deep Dive 2: Concentric Thermal Networks & The Critical Radius In the Thermal Systems (THERM-01) stage, we model: - Multi-layer series thermal resistance networks: $$R_{th} = \sum \frac{L_i}{k_i A} + \frac{1}{h A}$$ - Concentric cylindrical conduction and the Critical Radius of Insulation: $$r_{crit} = \frac{k}{h}$$ - Transient lumped capacitance cooling with automated Biot Number validation ($Bi < 0.1$). A classic engineering paradox taught in heat transfer is that adding insulation to a small pipe actually increases heat loss up until $r = r_{crit}$, after which heat loss begins to decrease. In Mecha Atelier, as you drag the insulation thickness slider, the 3D concentric cylinder dynamically resizes its geometry shells, updating radial isotherm shader colors and live resistance curves simultaneously. --- 🔨 Deep Dive 3: ASTM E23 Charpy Impact Tester in WebGL To model dynamic fracture behavior, we built an interactive virtual ASTM E23 Charpy Impact Testing Machine: - Heavy cast-iron base plinth with standardized 40mm anvil shoulders. - Kinematic pendulum swinging through release angle $\theta_1$ down to the notched specimen, rising to residual angle $\theta_2$. - Absorbed fracture energy computed via: $$KV = m g (h_1 - h_2)$$ - Ductile-to-Brittle Transition Temperature (DBTT): Users can chill the specimen down to cryogenic temperatures ($-60^\circ\text{C}$) or heat it to ($+100^\circ\text{C}$). At sub-zero temperatures, the specimen fractures cleanly with zero plastic shear lip (cleavage fracture). Above the transition temperature, the material undergoes extensive plastic necking and absorption before tear. --- 🔍 Solving the WebGL SEO & Generative Engine Optimization (GEO) Problem One major issue with WebGL applications is discoverability. Search engine crawlers (Googlebot, Bingbot) and AI search agents (PerplexityBot, ChatGPT-User, ClaudeBot) do not execute WebGL contexts or extract meaning from dynamic 3D scenes. To ensure Mecha Atelier remains indexable: 1. Semantic Fallback Crawl Layer: Underneath each WebGL stage, we render an accessible semantic layer containing standard HTML definitions, formula derivations, and summary data tables. 2. Schema.org JSON-LD: Every stage injects structured metadata for SoftwareApplication, EducationalOrganization, and TechArticle. 3. Dynamic OpenGraph Previews: Utilizing Next.js 14’s edge runtime (app/opengraph-image.tsx), we dynamically render 1200x630 branded social cards showing live discipline statistics. --- 💡 Key Takeaways 1. Analytical solvers beat FEA for real-time web interactivity: By evaluating exact closed-form solutions rather than discrete meshing loops, we keep 3D scenes locked at 60 FPS. 2. Zustand is essential for Three.js in React: Decoupling high-frequency slider state from React's top-level render tree prevents canvas tearing and dropped frames. 3. Design elevates engineering tools: Technical software doesn’t have to look gray and utilitarian. Crisp typography, blueprint gridlines, and subtle lighting make complex concepts exciting to explore. --- 🚀 Try It Yourself - Live Demo: Mecha Atelier - GitHub Repository: Fall-Of-The-Dawn/mecha-atelier I’d love to hear your feedback in the comments: What engineering or physics concept did you find hardest to visualize in school that you wish had an interactive 3D model?

    1 reaction
  2. Manual Render Invalidation: A Deliberate React Anti-Pattern While developing the morphing-scroll library, I arrived at an unusual way of organizing local data inside a React component. It was not an architecture I had designed in advance. I was experimenting with updates, gradually removing unnecessary state, and at some point the code became noticeably simpler than before. For me, it was a genuine discovery. Later, I extracted the mechanism into a useRerender hook and added it to my state manager, nexus-state. In this article, I want to show not only the result, but also the path that led me to it. The idea is to separate two operations that useState usually combines: 1. change the data; 2. tell React that the UI needs to update. The data can live in refs and be changed synchronously, while a single shared signal is used to update the UI. This goes against the conventional React model and is not suitable for every component. That is exactly why it is interesting to explore where such an anti-pattern can become a useful tool. Throughout this article, I will call this approach manual render invalidation. The data changes independently of React, and the developer explicitly signals when the current view should be considered stale and rebuilt. I coined this name for the article; it is not an established React term. !Image description From a Regular Counter to Redundant State Let us start with a regular counter: import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount((value) => value + 1)}> Clicks: {count} </button> ); } Here, useState is exactly the right tool. The value is used during rendering, and every change should update the button text. This code is familiar to almost every React developer, and there is nothing unusual about it yet. Now let us place the counter inside a product card. In addition to quantity, we need a selected product and a total price. We will deliberately write the first version in the most straightforward way: one useState for every changing value. const products = { keyboard: { name: "Keyboard", price: 80 }, mouse: { name: "Mouse", price: 40 }, display: { name: "Monitor", price: 350 }, }; function ProductCard() { const [productId, setProductId] = useState("keyboard"); const [quantity, setQuantity] = useState(1); const [total, setTotal] = useState(products.keyboard.price); function changeProduct(nextProductId) { setProductId(nextProductId); setTotal(products[nextProductId].price * quantity); } function changeQuantity(nextQuantity) { setQuantity(nextQuantity); setTotal(products[productId].price * nextQuantity); } return ( <section> <select value={productId} onChange={(event) => changeProduct(event.target.value)} > <option value="keyboard">Keyboard</option> <option value="mouse">Mouse</option> <option value="display">Monitor</option> </select> <button onClick={() => changeQuantity(Math.max(1, quantity - 1))}> − </button> <span>{quantity}</span> <button onClick={() => changeQuantity(quantity + 1)}>+</button> <p>Total: ${total.toLocaleString("en-US")}</p> </section> ); } The code works, but a review reveals an uncomfortable dependency. Every product or quantity change must also call setTotal. If another way to change the quantity appears later, the total can easily fall out of sync with the rest of the data. total has no independent source. It is always the selected product's price multiplied by the quantity, so there is no reason to store it: function ProductCard() { const [productId, setProductId] = useState("keyboard"); const [quantity, setQuantity] = useState(1); const product = products[productId]; const total = product.price * quantity; // The rest of the markup stays the same } This removes setTotal, both manual synchronization points, and the possibility of contradictory state. React also recommends avoiding redundant state. At first, this looked like an ordinary cleanup: find redundant state, replace it with a calculation, and reduce the number of values that need synchronization. But the refactoring raised a broader question. If one value does not need to be React state because it can be calculated, where is the actual boundary? Does React have to store the remaining data merely because it sometimes appears in JSX? Can we change it ourselves and ask React to render only when the result is ready to be shown? In React, it is easy to start associating every change with useState. As a result, new state often appears automatically, before we even ask whether React should own that value. This is where I began to wonder: what if I removed every useState, stored the data myself, and kept only one shared update mechanism? The product-card example is deliberately simple. In practice, this approach helped me while developing a complex React component built around scrolling. !Image description The Turning Point in morphing-scroll morphing-scroll had a different problem. The component manages scrolling, virtualization, dragging, inertia, and its own controls. To do that, it needs: - the current and target scroll positions; - viewport and content dimensions; - the visible item range; - the movement direction; - dragging and animation state; - identifiers of scheduled frames. The first obvious solution looks like this: const [scrollTop, setScrollTop] = useState(0); const [targetScrollTop, setTargetScrollTop] = useState(0); const [viewportHeight, setViewportHeight] = useState(0); const [visibleRange, setVisibleRange] = useState([0, 20]); const [isDragging, setIsDragging] = useState(false); const [isScrolling, setIsScrolling] = useState(false); The number of hooks alone does not explain the problem. Complexity appears when there are many state values and relationships emerge between them. A single scroll event changes the position, direction, and visible range. Starting a drag switches several flags. On every frame, the animation needs to read fresh data, calculate the next values, and sometimes update the JSX. A setter also does not change the value inside the handler that is currently running: function handleScroll(nextScrollTop) { setScrollTop(nextScrollTop); console.log(scrollTop); // value from the current render } This is how React state works: every render receives its own snapshot, and the setter queues the next one. That is a useful guarantee for a declarative interface. Inside the scrolling algorithm, however, I needed synchronously mutable data that handlers could access immediately. Several independent setters made the order of calculations harder to reason about. React batches multiple state updates into a single render. But batching only answers how many renders will happen. It does not separate changing the data from requesting a UI update: every setter still does both. Why Not useReducer? Related state can be grouped in useReducer. This removes multiple setters, leaves a single dispatch, and is the more conventional solution for most components. However, a reducer preserves React's underlying model: dispatch queues a new state snapshot and requests a render along with it. I needed something different in morphing-scroll. The internal model had to change synchronously, sometimes several times between frames, while React should only become involved when the accumulated result was ready to be displayed. The issue was therefore not only the number of setters. I needed to separate changing the data from publishing those changes to the interface. A Ref as the Component's Internal Model I grouped the related values into a single object inside useRef: const runtimeRef = useRef({ scrollTop: 0, targetScrollTop: 0, viewportHeight: 0, rowHeight: 40, visibleRange: [0, 20], isDragging: false, isScrolling: false, }); Now the handler can finish every calculation synchronously: function handleScroll(element) { const runtime = runtimeRef.current; runtime.scrollTop = element.scrollTop; runtime.viewportHeight = element.clientHeight; runtime.visibleRange = calculateVisibleRange( runtime.scrollTop, runtime.viewportHeight, runtime.rowHeight, ); runtime.isScrolling = true; } Each line immediately sees the result of the previous one. There are no separate setters, and there is no need to remember which render snapshot a value belongs to. One problem remains: changing ref.current does not trigger a render. React does not know that it should read visibleRange again. useRerender as a Separate Signal A function component can request another render through dummy state: import { useReducer } from "react"; function useRerender() { const [, rerender] = useReducer((version) => version + 1, 0); return rerender; } The version value is never used. Its only purpose is to make React start another render of the component. Now several changes can end with one explicit rerender: const rerender = useRerender(); function handleScroll(element) { const runtime = runtimeRef.current; runtime.scrollTop = element.scrollTop; runtime.viewportHeight = element.clientHeight; runtime.visibleRange = calculateVisibleRange( runtime.scrollTop, runtime.viewportHeight, runtime.rowHeight, ); rerender(); // explicitly update the UI at the end } This was the turning point for me. Until then, I had treated changing data and rendering as one operation: state changed, so React should start an update. It turned out that the two can be separated. First bring the internal model into a consistent state, then publish the result with a single signal. > Change the data first. Then decide when React should display it. The code unexpectedly became easier to understand. Instead of a large collection of value/setValue pairs, I had a regular object and one shared rerender. I no longer had to remember a separate setter name for every field or the order of several queued updates. Multiple mutations became a single, explicitly marked update boundary. This, rather than the three lines inside useRerender, was the key result of the experiment. The component became responsible for the timing of the update: it decides which changes should be included in the next render. This style is much closer to ordinary imperative JavaScript. We change the required data synchronously and involve React only when we want to display its current version in the UI. One Render per Frame scroll and pointermove events may fire several times between two browser paints. In that case, the signal can be limited to one call per frame: const frameRef = useRef(null); const rerender = useRerender(); function renderInNextFrame() { if (frameRef.current !== null) return; frameRef.current = requestAnimationFrame(() => { frameRef.current = null; rerender(); }); } function handleScroll(event) { const runtime = runtimeRef.current; runtime.scrollTop = event.currentTarget.scrollTop; runtime.visibleRange = calculateVisibleRange( runtime.scrollTop, runtime.viewportHeight, runtime.rowHeight, ); renderInNextFrame(); } If five events occur between frames, the ref retains the result of the last one, while React receives only one render request. This is not automatic batching. It is an update boundary defined by the component itself. A scheduled requestAnimationFrame should also be cancelled when the component unmounts. For morphing-scroll, this became the main advantage: the rate at which the internal data changes no longer has to match React's render rate. It also helped performance because the component did not request more renders than the browser could actually paint. !Image description What Do We Give Up? React treats refs as an escape hatch. The documentation recommends using them for values that do not participate in JSX and warns against reading ref.current during rendering. Manual invalidation deliberately crosses that boundary: JSX reads data from a ref, and useRerender makes React read it again. This comes with limitations: 1. If you forget to call rerender, the data changes but the interface remains stale. React no longer manages that connection for you. 2. Reading a mutable ref during rendering goes beyond React's standard contract. This code works through explicit invalidation, but the linter and newer React features rely on a different contract: rendering should be determined by props, state, and context. 3. A stable reference requires extra care with memoization. React.memo, useMemo, and reference equality checks do not notice mutations inside the object. It is safer to pass specific values or a separate update version to child components. 4. Ref changes do not appear in React DevTools as regular state updates, which makes them harder to trace while debugging. Effects have an additional caveat. ref.current is not reactive by itself. However, after changing it we call rerender, so during the next render React still compares the new dependency value with the previous one: useEffect(() => { doSomething(runtimeRef.current.scrollTop); }, [runtimeRef.current.scrollTop]); // reevaluated in the render triggered by rerender() This works specifically because of useRerender: it starts a render in which the dependency expression is evaluated again, and React compares the result using Object.is. If the ref changes without rerender, the effect never learns about it. exhaustive-deps may warn that a mutable value is not a valid reactive dependency. A modern eslint-plugin-react-hooks configuration may also enable the refs rule because the dependency is read during rendering. These warnings are correct for conventional React code: mutating a ref does not trigger a render. With manual invalidation, useRerender supplies the missing step, but the linter cannot infer that contract from the code. A local suppression becomes another explicit cost of the anti-pattern. Such an effect reacts only to values that reach a render, not to every ref mutation. Its callback also reads runtimeRef.current.scrollTop when the effect actually runs, so it may see a newer value than the one React compared in the dependency array. If every mutation or the exact snapshot of a particular update matters, invoke the side effect next to the data change or use a fully reactive source instead. !Image description Rewriting the Product Card with the Anti-Pattern At the beginning of the article, we removed only the derived total, while productId and quantity remained in React state. For the sake of the experiment, let us take the idea all the way and rewrite the card without a single useState. The products object containing names and prices remains the same one declared outside the component in the first example. function ProductCard() { const rerender = useRerender(); const modelRef = useRef({ productId: "keyboard", quantity: 1, }); const model = modelRef.current; const total = products[model.productId].price * model.quantity; return ( <section> <select value={model.productId} onChange={(event) => { model.productId = event.target.value; rerender(); }} > <option value="keyboard">Keyboard</option> <option value="mouse">Mouse</option> <option value="display">Monitor</option> </select> <button onClick={() => { model.quantity = Math.max(1, model.quantity - 1); rerender(); }} > − </button> <span>{model.quantity}</span> <button onClick={() => { model.quantity += 1; rerender(); }} > + </button> <p>Total: ${total.toLocaleString("en-US")}</p> </section> ); } model.productId stores the key of the selected product, for example "keyboard". The expression products[model.productId] finds the product in the catalog by that key, after which its price is multiplied by the quantity. total remains a regular constant because it is calculated from the source data during every render. Moving it into another ref would bring no meaningful benefit and would reintroduce the manual synchronization that we removed at the beginning of the article. The code is straightforward: data changes through regular assignments, and every field shares the same rerender. Before calling it, we can change one field or several at once. !Image description Where Is This Approach Most Useful? I would consider refs together with useRerender for components that have their own imperative model: - virtualized lists and complex scrolling; - drag and drop; - canvas and WebGL; - animation loops; - high-frequency pointer events; - DOM measurements and synchronization with browser APIs. The advantage is most noticeable when data changes more often than JSX should update, or when several values need to change consistently before a render. Another reason to consider the approach is a large amount of local state in one component. Every new value/setValue pair adds another name and another update path that must be kept in mind. If that makes the code harder to read and control, one data object and a shared rerender can noticeably reduce the cognitive load. There is no universal number of useState calls after which a component must be rewritten. What matters is the practical result: which model describes the component more accurately, and which one makes data changes easier to follow. !Image description How Original Is This Technique? I arrived at this solution independently while working on morphing-scroll. I was not searching for an existing pattern or trying to reproduce someone else's implementation. The idea emerged from experiments with a real component and immediately proved useful in practice. That is why the moment it worked felt like a revelation. At the same time, the individual parts of the approach existed before. The old React Hooks FAQ demonstrates an almost identical useReducer counter, and various libraries provide hooks named useForceUpdate, useUpdate, or useRerender. The mechanism for forcing a render is not new. There are also closer architectural parallels. useSyncExternalStore separates an external data source from the signal sent to subscribers. MobX and Valtio let a model change independently of React while automatically tracking which components need to update. In React Three Fiber, imperative changes can be followed by invalidate() to request another frame. The last example is especially close to how this approach works with scrolling and requestAnimationFrame. However, among the examples I found, I did not see an ordinary local React component described in exactly this way: refs serve as a complete model, while one rerender becomes an explicit publication boundary. Similar code may have been written before, but I am not claiming to have invented the hook. What matters to me is the independently discovered way of organizing the component around it. The name "manual render invalidation" describes this complete model. If the current data already lives outside React state, there is no reason to present a technical render counter as domain state. Its actual job is to mark the current view as stale and ask React to build a new one. That is why I added this primitive to nexus-state: const rerender = nexus.useRerender(); runtimeRef.current.value = nextValue; rerender(); !Image description Conclusion The path to useRerender did not begin with a desire to invent a new hook. While developing morphing-scroll, working with many related state values became so confusing that I started removing unnecessary useState calls one by one. As a result, most of the runtime data moved into refs, while a separate useRerender hook remained solely as a signal telling React to render the current model. This experience changed how I think about useState. It is not merely a container with a value/setValue pair, but a contract with React: React stores a snapshot of the data and decides when to show the next one. If a component accumulates so many state variables that navigating their values and setters becomes difficult, that is already a valid reason to consider another model. This is especially true when the values are related, need to change synchronously, or the data changes more often than the interface needs to render. In those cases, a more imperative approach with useRerender may be easier to read and control. useRerender showed me another model: data can change as a regular JavaScript object, while rendering becomes a separate explicit operation. We can update several values, inspect the result, wait for the next frame if necessary, and only then ask React to update the interface. For complex scrolling, this was not a workaround, but the most direct description of what was happening. When this version first worked, I was surprised by how little machinery remained in the code. I removed several abstractions and gained more control: one mutable model, one way to request a render, and a clear boundary between calculation and display. That is the essence of manual render invalidation for me. The main idea is not to replace every useState, but to stop treating data changes and interface updates as one indivisible operation. I hope this path helps other developers look at React from a slightly different angle. Related Articles If you would like to see the context from which this approach grew, you can also read my other articles: - The story of morphing-scroll, my npm library; - How I Wrote My Own State Manager. !Image description

  3. Ручная инвалидация рендера как осознанный антипаттерн в React Во время разработки библиотеки morphing-scroll я пришёл к необычному способу организации локальных данных в React-компоненте. Это не было заранее придуманной архитектурой. Я экспериментировал с обновлениями, постепенно убирал лишние состояния и в какой-то момент получил код, который оказался заметно проще прежнего. Для меня это стало настоящим открытием. Позже я вынес найденный механизм в хук useRerender и добавил в свой стейт-менеджер nexus-state. В этой статье я хочу показать не только результат, но и путь к нему. Идея состоит в том, чтобы разделить две операции, которые обычно объединяет useState: 1. изменить данные; 2. сообщить React, что интерфейс нужно обновить. Данные можно хранить в refs и изменять синхронно, а для обновления интерфейса использовать один общий сигнал. Это противоречит привычной модели React и подходит далеко не каждому компоненту. Но именно поэтому интересно разобраться, где такой антипаттерн становится полезным инструментом. Дальше я буду называть этот подход ручной инвалидацией рендера. Данные изменяются независимо от React, а разработчик сам сообщает, когда текущее отображение нужно признать устаревшим и построить заново. Это название придумано для статьи и не является общепринятым термином React. От обычного счётчика к лишнему состоянию Начнём с обычного счётчика: import { useState } from "react"; function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount((value) => value + 1)}> Нажатий: {count} </button> ); }sourcecraft.dev Здесь useState решает задачу точно. Значение участвует в рендере, а каждое изменение должно обновить текст кнопки. Такой код знаком почти каждому React-разработчику и пока не содержит ничего необычного. Теперь поместим счётчик в карточку товара. Кроме количества, нам понадобятся выбранный товар и итоговая стоимость. Первую версию намеренно напишем прямолинейно: для каждого изменяемого значения создадим отдельный useState. const products = { keyboard: { name: "Клавиатура", price: 8_000 }, mouse: { name: "Мышь", price: 4_000 }, display: { name: "Монитор", price: 35_000 }, }; function ProductCard() { const [productId, setProductId] = useState("keyboard"); const [quantity, setQuantity] = useState(1); const [total, setTotal] = useState(products.keyboard.price); function changeProduct(nextProductId) { setProductId(nextProductId); setTotal(products[nextProductId].price * quantity); } function changeQuantity(nextQuantity) { setQuantity(nextQuantity); setTotal(products[productId].price * nextQuantity); } return ( <section> <select value={productId} onChange={(event) => changeProduct(event.target.value)} > <option value="keyboard">Клавиатура</option> <option value="mouse">Мышь</option> <option value="display">Монитор</option> </select> <button onClick={() => changeQuantity(Math.max(1, quantity - 1))}> − </button> <span>{quantity}</span> <button onClick={() => changeQuantity(quantity + 1)}>+</button> <p>Итого: {total.toLocaleString("ru-RU")} ₽</p> </section> ); }sourcecraft.dev Этот код работает, но во время проверки мы заметим неприятную связь. Любое изменение товара или количества требует не забыть вызвать setTotal. Если позже появится ещё один способ изменить количество, итог легко перестанет соответствовать остальным данным. У total нет самостоятельного источника. Он всегда равен цене выбранного товара, умноженной на количество. Значит, хранить его не нужно: function ProductCard() { const [productId, setProductId] = useState("keyboard"); const [quantity, setQuantity] = useState(1); const product = products[productId]; const total = product.price * quantity; // остальная разметка остаётся прежней }sourcecraft.dev Так исчезли setTotal, обе ручные синхронизации и возможность получить противоречивое состояние. React также рекомендует не хранить производные данные в state. Сначала это выглядело как обычное приведение кода в порядок: нашёл лишний state, заменил его вычислением и уменьшил число значений, которые нужно синхронизировать. Но за этим рефакторингом обнаружился более общий вопрос. Если одно значение не обязано быть React state, потому что его можно вычислить, то где вообще проходит граница? Обязательно ли React должен хранить остальные данные только потому, что они участвуют в JSX? Можно ли изменять их самостоятельно, а рендер запрашивать лишь тогда, когда результат действительно пора показать? В React легко привыкнуть связывать любое изменение с useState. Из-за этого новый state часто появляется автоматически, ещё до вопроса о том, должен ли React вообще хранить это значение. Именно здесь я задумался: а что, если попробовать убрать все useState, хранить данные самостоятельно и оставить только один общий механизм обновления? Пример с магазином намеренно упрощён. На практике этот подход помог мне при разработке сложного React-компонента, связанного со скроллом. Переломный момент в morphing-scroll В morphing-scroll задача была другой. Компонент управляет прокруткой, виртуализацией, перетаскиванием, инерцией и собственными элементами управления. Для работы ему нужны: - текущая и целевая позиция прокрутки; - размеры области просмотра и содержимого; - видимый диапазон элементов; - направление движения; - состояние перетаскивания и анимации; - идентификаторы запланированных кадров. Первое очевидное решение выглядит так: const [scrollTop, setScrollTop] = useState(0); const [targetScrollTop, setTargetScrollTop] = useState(0); const [viewportHeight, setViewportHeight] = useState(0); const [visibleRange, setVisibleRange] = useState([0, 20]); const [isDragging, setIsDragging] = useState(false); const [isScrolling, setIsScrolling] = useState(false);sourcecraft.dev Количество хуков само по себе ещё не объясняет проблему. Сложность появляется, когда состояний становится много и между ними возникают связи. Одно событие scroll меняет позицию, направление и видимый диапазон. Начало drag переключает несколько флагов. Анимация должна на каждом кадре прочитать свежие данные, вычислить следующие и иногда обновить JSX. Сеттер при этом не меняет значение в уже выполняющемся обработчике: function handleScroll(nextScrollTop) { setScrollTop(nextScrollTop); console.log(scrollTop); // значение из текущего рендера }sourcecraft.dev Так работает React state: каждый рендер получает собственный снимок, а сеттер ставит следующий снимок в очередь. Для декларативного интерфейса это полезная гарантия. Внутри скролла мне, наоборот, требовались синхронно изменяемые данные, доступные обработчикам прямо сейчас. Несколько отдельных сеттеров затрудняли предсказуемый порядок вычислений. React объединяет несколько обновлений состояния в один рендер. Но batching отвечает только на вопрос, сколько рендеров произойдёт. Он не отделяет изменение данных от запроса на обновление интерфейса: каждый сеттер по-прежнему делает и то и другое. Почему не useReducer Связанные состояния можно собрать в useReducer. Это уберёт множество сеттеров, оставит один dispatch и для большинства компонентов будет более стандартным решением. Но reducer сохраняет основную модель React: dispatch ставит в очередь новый снимок state и вместе с ним запрашивает рендер. В morphing-scroll мне требовалось другое. Внутренняя модель должна была изменяться синхронно, иногда несколько раз между кадрами, а React нужно было подключать только тогда, когда накопленный результат пора показать. Поэтому вопрос был не только в количестве сеттеров. Мне нужно было разделить изменение данных и публикацию этих изменений в интерфейсе. Ref как внутренняя модель компонента Я собрал связанные значения в один объект внутри useRef: const runtimeRef = useRef({ scrollTop: 0, targetScrollTop: 0, viewportHeight: 0, rowHeight: 40, visibleRange: [0, 20], isDragging: false, isScrolling: false, });sourcecraft.dev Теперь обработчик может закончить все вычисления синхронно: function handleScroll(element) { const runtime = runtimeRef.current; runtime.scrollTop = element.scrollTop; runtime.viewportHeight = element.clientHeight; runtime.visibleRange = calculateVisibleRange( runtime.scrollTop, runtime.viewportHeight, runtime.rowHeight, ); runtime.isScrolling = true; }sourcecraft.dev Следующая строка сразу видит результат предыдущей. Нет нескольких сеттеров и не нужно помнить, к снимку какого рендера относится значение. Остаётся одна проблема: изменение ref.current не запускает рендер. React не узнает, что visibleRange пора перечитать. useRerender как отдельный сигнал Повторный рендер функционального компонента можно запросить через фиктивное состояние: import { useReducer } from "react"; function useRerender() { const [, rerender] = useReducer((version) => version + 1, 0); return rerender; }sourcecraft.dev Значение version нигде не используется. Оно нужно только для того, чтобы React запустил новый рендер компонента. Теперь несколько изменений можно завершить одним явным rerender: const rerender = useRerender(); function handleScroll(element) { const runtime = runtimeRef.current; runtime.scrollTop = element.scrollTop; runtime.viewportHeight = element.clientHeight; runtime.visibleRange = calculateVisibleRange( runtime.scrollTop, runtime.viewportHeight, runtime.rowHeight, ); rerender(); // явно обновляем наш интерфейс в конце }sourcecraft.dev Именно здесь для меня произошёл перелом. До этого я воспринимал обновление данных и рендер как одну операцию: изменился state, значит React должен начать обновление. Оказалось, эти действия можно разъединить. Сначала привести внутреннюю модель в согласованное состояние, а затем одним сигналом опубликовать результат. Сначала изменить данные. Потом решить, когда React должен их показать. Неожиданно код стал проще для понимания. Вместо большого набора пар value/setValue остался обычный объект и один общий rerender. Мне больше не нужно было помнить отдельное имя сеттера для каждого поля и порядок нескольких запланированных обновлений. Несколько мутаций превратились в одну явно обозначенную границу обновления. Именно это, а не три строки внутри useRerender, стало ключевым результатом эксперимента. Ответственность за момент обновления перешла к компоненту: он сам решает, какие изменения должны войти в следующий рендер. Такой стиль гораздо ближе к обычному императивному JavaScript. Мы синхронно изменяем нужные данные, а React подключаем только тогда, когда хотим отобразить их актуальную версию в UI. Один рендер на кадр События scroll и pointermove могут приходить несколько раз между двумя отрисовками браузера. В этом случае сигнал можно ограничить одним вызовом на кадр: const frameRef = useRef(null); const rerender = useRerender(); function renderInNextFrame() { if (frameRef.current !== null) return; frameRef.current = requestAnimationFrame(() => { frameRef.current = null; rerender(); }); } function handleScroll(event) { const runtime = runtimeRef.current; runtime.scrollTop = event.currentTarget.scrollTop; runtime.visibleRange = calculateVisibleRange( runtime.scrollTop, runtime.viewportHeight, runtime.rowHeight, ); renderInNextFrame(); }sourcecraft.dev Если между кадрами произошло пять событий, ref сохранит результат последнего, а React получит один запрос на рендер. Это не автоматический batching, а граница обновления, которую задаёт сам компонент. На размонтировании запланированный requestAnimationFrame также нужно отменить. Для morphing-scroll это оказалось главным преимуществом: скорость изменения внутренних данных больше не обязана совпадать с частотой рендера React. Это также помогло производительности: компонент не запрашивал больше рендеров, чем браузер мог показать кадров. Что мы за это отдаём React рассматривает refs как escape hatch. Документация рекомендует хранить в них значения, которые не участвуют в JSX, и предупреждает о чтении ref.current во время рендера. Ручная инвалидация сознательно пересекает эту границу: JSX читает данные из ref, а useRerender заставляет React перечитать их. Из этого следуют ограничения: 1. Если забыть вызвать rerender, данные изменятся, а интерфейс останется прежним. React уже не контролирует эту связь за разработчика. 2. Чтение изменяемого ref во время рендера выходит за стандартный контракт React. Такой код работает за счёт явной инвалидации, но линтер и новые возможности React опираются на другой договор: результат рендера должен определяться props, state и context. 3. Стабильная ссылка требует внимания при мемоизации. React.memo, useMemo или сравнение по ссылке не заметят внутренних мутаций объекта. В дочерний компонент лучше передавать конкретные значения или отдельную версию обновления. 4. Изменения refs не отображаются в React DevTools как обычные обновления state, поэтому их сложнее отслеживать при отладке. Есть и отдельная особенность эффектов. ref.current сам по себе не является реактивным значением. Но после его изменения мы вызываем rerender, поэтому на следующем рендере React всё же сравнит новое значение зависимости с предыдущим: useEffect(() => { doSomething(runtimeRef.current.scrollTop); }, [runtimeRef.current.scrollTop]); // выражение вычислится заново после rerender()sourcecraft.dev Этот код сработает именно благодаря useRerender: он запускает рендер, во время которого выражение в массиве зависимостей вычисляется заново, а React сравнивает результат через Object.is. Если ref изменить без rerender, эффект об этом не узнает. exhaustive-deps может предупредить, что изменяемое значение не является допустимой реактивной зависимостью. Современная конфигурация eslint-plugin-react-hooks также может задействовать правило refs, потому что зависимость читается во время рендера. Для обычного React-кода эти предупреждения справедливы: мутация ref не вызывает рендер. В ручной инвалидации этот недостающий шаг выполняет useRerender, но линтер не может вывести такой договор из кода. Локальное подавление предупреждения здесь становится ещё одной явной ценой антипаттерна. Такой эффект реагирует не на каждую мутацию ref, а только на значения, которые дошли до очередного рендера. Кроме того, callback читает runtimeRef.current.scrollTop уже во время выполнения и может увидеть более новое значение, чем то, которое React сравнивал в массиве зависимостей. Если важна каждая мутация или точный снимок конкретного обновления, побочный эффект лучше вызвать рядом с изменением данных либо использовать полноценный реактивный источник. Перепишем магазин через антипаттерн В начале статьи мы удалили только производный total, а productId и quantity оставили в React state. Теперь для интереса доведём мысль до конца и перепишем карточку без единого useState. Объект products с названиями и ценами остаётся тем же: он объявлен вне компонента в первом примере. function ProductCard() { const rerender = useRerender(); const modelRef = useRef({ productId: "keyboard", quantity: 1, }); const model = modelRef.current; const total = products[model.productId].price * model.quantity; return ( <section> <select value={model.productId} onChange={(event) => { model.productId = event.target.value; rerender(); }} > <option value="keyboard">Клавиатура</option> <option value="mouse">Мышь</option> <option value="display">Монитор</option> </select> <button onClick={() => { model.quantity = Math.max(1, model.quantity - 1); rerender(); }} > − </button> <span>{model.quantity}</span> <button onClick={() => { model.quantity += 1; rerender(); }} > + </button> <p>Итого: {total.toLocaleString("ru-RU")} ₽</p> </section> ); }sourcecraft.dev model.productId хранит ключ выбранного товара, например "keyboard". Выражение products[model.productId] находит по этому ключу товар в каталоге, после чего его цена умножается на количество. total остаётся обычной константой, потому что вычисляется из исходных данных при каждом рендере. Переносить его в отдельный ref нет смысла: это не даст заметного выигрыша, зато вернёт ручную синхронизацию, от которой мы избавились в начале статьи. Код получился прямолинейным: данные изменяются обычным присваиванием, а для всех полей используется один rerender. Перед его вызовом можно изменить одно поле или сразу несколько. Где подход особенно полезен Я бы рассматривал refs вместе с useRerender для компонентов, у которых есть собственная императивная модель: - виртуализированные списки и сложный скролл; - drag-and-drop; - canvas и WebGL; - анимационные циклы; - pointer-события высокой частоты; - DOM-измерения и синхронизация с браузерными API. Преимущество заметнее всего там, где данные меняются чаще, чем должен обновляться JSX, или перед рендером нужно согласованно изменить несколько значений. Ещё один повод рассмотреть этот подход — большое количество локальных состояний в одном компоненте. Каждая новая пара value/setValue добавляет имя и отдельный путь обновления, которые приходится держать в голове. Если из-за этого код становится труднее читать и контролировать, один объект с данными и общий rerender могут заметно снизить когнитивную нагрузку. Само количество useState не задаёт универсального порога, после которого код обязательно нужно переписывать. Важнее практический результат: какая модель точнее описывает компонент и в какой из них проще проследить изменение данных. Насколько этот приём оригинален К этому решению я пришёл самостоятельно во время работы над morphing-scroll. Я не искал готовый паттерн и не пытался повторить чужую реализацию: идея появилась из экспериментов с реальным компонентом и сразу доказала свою пользу на практике. Именно поэтому момент, когда всё заработало, был для меня озарением. При этом отдельные части подхода существовали раньше. В старом React Hooks FAQ показан почти такой же счётчик на useReducer, а в библиотеках встречаются хуки с названиями useForceUpdate, useUpdate и useRerender. Сам механизм принудительного рендера не новый. Есть и более близкие архитектурные параллели. useSyncExternalStore отделяет внешний источник данных от сигнала подписчикам. MobX и Valtio позволяют изменять модель отдельно от React, но автоматически отслеживают, какие компоненты нужно обновить. А в React Three Fiber после императивных изменений можно вызвать invalidate(), чтобы запросить новый кадр. Последний пример особенно близок к тому, как этот подход работает со скроллом и requestAnimationFrame. Но среди найденных примеров я не встретил описания обычного локального React-компонента, в котором эта идея была бы сформулирована именно так: refs используются как полноценная модель, а один rerender становится явной границей её публикации. Возможно, подобный код уже писали и раньше, но мне важна не претензия на изобретение хука, а самостоятельно найденный способ организации компонента. Название «ручная инвалидация рендера» описывает именно эту полную модель. Если актуальные данные уже хранятся вне React state, технический счётчик рендеров не стоит выдавать за предметное состояние. Его настоящая задача состоит в том, чтобы пометить текущее отображение устаревшим и попросить React построить новое. Поэтому я добавил такой примитив в nexus-state: const rerender = nexus.useRerender(); runtimeRef.current.value = nextValue; rerender();sourcecraft.dev Итог Путь к useRerender начался не с желания придумать новый хук. Во время разработки morphing-scroll работа с множеством связанных состояний стала настолько запутанной, что я начал убирать лишние useState один за другим. Так основная runtime-модель стала храниться в refs, а отдельный useUpdate остался сигналом, сообщающим React, что её пора отобразить заново. Этот опыт изменил моё понимание самого useState. Это не просто контейнер с парой value/setValue, а договор с React: React хранит снимок данных и решает, когда показать следующий. Если таких состояний становится настолько много, что в их значениях и сеттерах трудно ориентироваться, это уже полноценный повод рассмотреть другую модель. Особенно когда состояния связаны, должны изменяться синхронно или данные меняются чаще, чем нужно перерисовывать интерфейс. В таких случаях более императивный подход с useRerender может оказаться проще для чтения и контроля. useRerender показал мне ещё одну модель: данные могут изменяться как обычный JavaScript-объект, а рендер становится отдельной явной операцией. Сначала можно обновить несколько значений, проверить результат, при необходимости дождаться следующего кадра и только потом попросить React обновить интерфейс. Для сложного скролла это оказалось не обходным путём, а самым прямым описанием происходящего. Когда этот вариант впервые заработал, меня удивило, насколько мало механики осталось в коде. Я убрал несколько абстракций и получил больше контроля: одна изменяемая модель, один способ запросить рендер и понятная граница между вычислением и отображением. В этом для меня и состоит суть ручной инвалидации рендера. Главная идея здесь не в том, чтобы заменить каждый useState, а в возможности перестать считать изменение данных и обновление интерфейса одной неделимой операцией. Надеюсь, этот путь поможет и другим разработчикам немного иначе взглянуть на React. Связанные статьи Если хотите увидеть контекст, из которого вырос этот подход, прочитайте также другие мои статьи: - История npm-библиотеки morphing-scroll - О том, как я написал свой стейт-менеджер

  4. AI 모션그래픽 저장소 20개 모음 모션그래픽 제작에 쓰는 저장소 20개를 기획·에셋·움직임·입체 장면·출력 순서로 정리했습니다. 필요한 도구만 조합하고 각 저장소의 설치 안내와 이용 조건을 확인하면 됩니다. 도구·자료 주소 penpot/penpot excalidraw/excalidraw fontsource/fontsource lucide-icons/lucide svg/svgo lovell/sharp airbnb/lottie-web rive-app/rive-wasm motiondivision/motion greensock/GSAP juliangarnier/anime theatre-js/theatre mrdoob/three.js pmndrs/react-three-fiber pmndrs/drei pmndrs/postprocessing remotion-dev/remotion remotion-dev/skills FFmpeg/FFmpeg Vanilagy/mediabunny #모션그래픽 #개발도구 #AI활용 #투더제이 [en-US] 20 AI Motion Graphics Resources Collection We have organized 20 repositories used for motion graphics production in the order of Planning, Assets, Motion, 3D Scenes, and Output. Simply combine the tools you need and check the installation instructions and terms of use for each repository. Tool/Resource Addresses penpot/penpot excalidraw/excalidraw fontsource/fontsource lucide-icons/lucide svg/svgo lovell/sharp airbnb/lottie-web rive-app/rive-wasm motiondivision/motion greensock/GSAP gi thub.com/juliangarnier/anime theatre-js/theatre mrdoob/three.js pmndrs/react-three-fiber pmndrs/drei pmndrs/postprocessing remotion-dev/remotion remotion-dev/skills FFmpeg/FFmpeg Vanilagy/mediabunny #MotionGraphics #DevelopmentTools #AIUtilization #ToTheJ

    81 likes · 2 comments · 2,503 views
  5. @infinterenders @acdlite Glad to see some people interested in this. I had to do this, caching, and a parallel scheduler that harmonized CPU/GPU work at my last job that I wasn’t able to upstream in whole. Lots of resistance from web folks even though provably optimal in our case. pmndrs/directed

    3 likes · 65 views
  6. Looks like someone discovered Klipp already 👀 Check it out for yourself 👉 pmndrs/klipp

    10 likes · 614 views
  7. 把大佬的宝藏做成了一个开箱即用的skill,大家可以来玩呀 GitHub:JuniverseLi/motion-studio-skill

    2 likes · 1 repost · 8 views
  8. How Japanese Chinchiro and Chinese Bo Bing dice games are scored (and simulating them with 3D physics in the browser) Two of the most fun "roll and shout" dice games in East Asia are Chinchiro (チンチロ / チンチロリン) from Japan and Bo Bing (博饼, "cake gambling") from Xiamen, China, traditionally played around the Mid-Autumn Festival. Both are simple to play, but their scoring tables hide some interesting probability. In this post I'll go through the rules, the combination tables, the exact probabilities (computed by enumerating every outcome), and finally how to make the dice actually tumble in the browser with a physics engine. Part 1: Chinchiro (3 dice) Rules - Players roll three dice into a bowl (traditionally a rice bowl). - Each player gets up to three tries to make a scoring combination. The first scoring roll stands. - A die that jumps out of the bowl is usually an immediate loss (shonben). - In the classic banker game, each player's hand is compared to the banker's, and some hands pay multiples of the bet. Combination table | Hand | Japanese | Dice | Typical payout | |---|---|---|---| | Pinzoro | ピンゾロ | 1-1-1 | wins 5x | | Arashi / Zorome | 嵐・ゾロ目 | triple 2–6 | wins 3x | | Shigoro | シゴロ | 4-5-6 | wins 2x | | Point (me) | 目 | a pair + one odd die; the odd die is the point (1–6) | compare points | | Menashi | 目なし | anything else | no hand (roll again, up to 3 times) | | Hifumi | ヒフミ | 1-2-3 | loses 2x | Payouts vary between house rules; ranks are almost always the same: Pinzoro > other triples > 4-5-6 > point 6 … point 1 > no hand > 1-2-3. Probabilities Three dice have 6³ = 216 equally likely outcomes. | Hand | Count | Probability | |---|---|---| | 1-1-1 | 1 | 0.46% | | Triple 2–6 | 5 | 2.31% | | 4-5-6 (any order) | 3! = 6 | 2.78% | | 1-2-3 (any order) | 6 | 2.78% | | Point (pair + odd die) | 6 × 5 × 3 = 90 | 41.67% | | – each specific point 1…6 | 15 | 6.94% | | No hand | 108 | 50.00% | Exactly half of all single rolls are "no hand". With three tries, the chance of ending with no hand is (1/2)³ = 12.5%, and the chance of eventually making something is 87.5%. function chinchiro(d) { const [a, b, c] = [...d].sort((x, y) => x - y); if (a === c) return a === 1 ? 'pinzoro' : 'arashi'; if (a === 4 && b === 5 && c === 6) return 'shigoro'; if (a === 1 && b === 2 && c === 3) return 'hifumi'; if (a === b) return 'point ' + c; if (b === c) return 'point ' + a; return 'menashi'; } // Exact enumeration const tally = {}; for (let x = 1; x <= 6; x++) for (let y = 1; y <= 6; y++) for (let z = 1; z <= 6; z++) { const k = chinchiro([x, y, z]); tally[k] = (tally[k] || 0) + 1; } console.log(tally); // menashi: 108, point N: 15 each, ... Part 2: Bo Bing (6 dice) Rules - Players take turns throwing six dice into a large porcelain bowl. - Each throw is checked against the prize table; if it matches a tier, the player takes a prize (traditionally mooncakes) from that tier. - Fours are special: the 4 face is red, so the number of fours drives most tiers. - The game ends when all prizes are claimed. The top prize, Zhuangyuan, can be "stolen" by a later, higher Zhuangyuan roll. Combination table (from highest to lowest) | Tier | Chinese | Condition | |---|---|---| | Zhuangyuan (top scholar) | 状元 | 4 fours, or 5 of a kind, or 6 of a kind (sub-ranked: 6 fours > 6 of a kind > 4 fours + 2 ones > 5 fours > 5 of a kind > 4 fours) | | Duitang (straight) | 对堂 | 1-2-3-4-5-6 | | Sanhong (three reds) | 三红 | exactly 3 fours | | Sijin (four of a kind) | 四进 | 4 of the same non-4 face | | Erju (two reds) | 二举 | exactly 2 fours (and not Sijin) | | Yixiu (one red) | 一秀 | exactly 1 four (and nothing higher) | | — | — | anything else: no prize | Probabilities Six dice have 6⁶ = 46,656 outcomes. Counting by k, the number of fours: - Zhuangyuan: six 4s (1) + five 4s (6 × 5 = 30) + four 4s (C(6,4) × 5² = 375) + five of a kind of another face (5 × 6 × 5 = 150) + six of a kind of another face (5) = 561 - Duitang: 6! = 720 - Sanhong: C(6,3) × 5³ = 2,500 - Sijin: 5 faces × C(6,4) × 5² = 1,875 (this includes four-of-a-kind with one or two fours alongside, which still ranks as Sijin) - Erju: C(6,2) × 5⁴ = 9,375, minus the 75 where the other four dice are a four of a kind = 9,300 - Yixiu: 6 × 5⁵ = 18,750, minus 720 straights, minus 30 five-of-a-kinds, minus 600 four-of-a-kinds = 17,400 - Nothing: the rest = 14,300 | Tier | Count | Probability | About 1 in | |---|---|---|---| | Zhuangyuan | 561 | 1.20% | 83 | | Duitang | 720 | 1.54% | 65 | | Sanhong | 2,500 | 5.36% | 18.7 | | Sijin | 1,875 | 4.02% | 24.9 | | Erju | 9,300 | 19.93% | 5.0 | | Yixiu | 17,400 | 37.29% | 2.7 | | No prize | 14,300 | 30.65% | 3.3 | Two fun observations: 1. Sijin is rarer than Sanhong but ranks below it. The table is driven by tradition (the color red and the imperial exam metaphor), not strictly by probability. 2. Roughly 69% of throws win something, which keeps the game lively for a family table. A verification script that enumerates all 46,656 outcomes: function boBing(d) { const c = Array(7).fill(0); d.forEach(v => c[v]++); const max = Math.max(...c.slice(1)); const fours = c[4]; if (fours >= 4 || max >= 5) return 'zhuangyuan'; if (c.slice(1).every(n => n === 1)) return 'duitang'; if (fours === 3) return 'sanhong'; if (max === 4) return 'sijin'; if (fours === 2) return 'erju'; if (fours === 1) return 'yixiu'; return 'none'; } const t = {}; for (let i = 0; i < 6 ** 6; i++) { const d = []; let n = i; for (let j = 0; j < 6; j++) { d.push(n % 6 + 1); n = Math.floor(n / 6); } const k = boBing(d); t[k] = (t[k] || 0) + 1; } console.log(t); // { zhuangyuan: 561, duitang: 720, sanhong: 2500, sijin: 1875, // erju: 9300, yixiu: 17400, none: 14300 } Part 3: Rolling the dice with 3D physics in the browser Math.random() gives perfectly fair dice, but it's not much fun to watch. For a "real" feel you can simulate rigid bodies and read the result from the final orientation: 1. Physics world – use a library like cannon-es or Rapier (WASM). Create a static floor plus walls (or a bowl built from a few static shapes) and one Box body per die. 2. Throw – give each die a random position above the bowl, a random rotation, a linear velocity toward the center, and a random angular velocity. Step the world at a fixed timestep (e.g. 1/60 s) and copy body transforms to your Three.js meshes each frame. 3. Settle detection – consider the roll finished when every body's linear and angular speed stays below a small threshold for a few frames (or let the engine put bodies to sleep). 4. Read the face – rotate each face's local normal (+X, −X, +Y, −Y, +Z, −Z) by the body's quaternion and pick the one whose world-space normal has the largest dot product with the up vector (0, 1, 0). Map that face to its pip value. 5. Edge cases – if the best dot product is below ~0.9 the die is "cocked" against a wall; nudge it or re-roll. For Chinchiro, a die whose final position is outside the bowl radius is a shonben (instant loss), which a physics sim models for free. const faces = [ { n: new CANNON.Vec3(0, 1, 0), v: 1 }, { n: new CANNON.Vec3(0, -1, 0), v: 6 }, { n: new CANNON.Vec3(1, 0, 0), v: 2 }, { n: new CANNON.Vec3(-1, 0, 0), v: 5 }, { n: new CANNON.Vec3(0, 0, 1), v: 3 }, { n: new CANNON.Vec3(0, 0, -1), v: 4 }, ]; function readDie(body) { let best = null, bestDot = -Infinity; for (const f of faces) { const w = body.quaternion.vmult(f.n); if (w.y > bestDot) { bestDot = w.y; best = f.v; } } return { value: best, cocked: bestDot < 0.9 }; } Because the initial conditions are random and the dynamics are chaotic, the resulting face distribution is very close to uniform, and you can sanity-check it by running a few thousand headless throws and comparing the counts against the tables above. Wrap-up - Chinchiro: 216 outcomes, half of single rolls are "no hand", 1-1-1 is 1/216. - Bo Bing: 46,656 outcomes, Zhuangyuan about 1.2%, and about 69% of throws win a prize. - Physics-based dice add drama (and the occasional die flying out of the bowl) without hurting fairness. If you want to try physics-based dice rolling in your browser, I built a free set of 3D dice tools here: 3droll.com

  9. @MaximeHeckel @ggsimm The docs suck, Im working on it this week github.com/pmndrs/upscaler/blob/main/docs/tempora… but your agents would get it..

    3 likes · 25 views
  10. The found our stealth library for optimizations~~~ 👁‍🗨 pmndrs/labs

    45 likes · 2,903 views
Trending awards, every completed day and week
Gold
01st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
0Owned by pmndrs now

No trending awards yet

None of the repositories pmndrs owns has finished in the top three of a completed trending day or week, by stars, forks or PR authors. See who is trending now.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.