Skip to content

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Christmas credenza — original tight-lights app

Extracted directly from the live app attached to this post:

Tightened the strand against the branches, keeping uneven wrapping and varied bulb angles.

The scene is christmas-credenza-tight-3d. Its 28,447-byte JavaScript module was recovered through read-only inspection of the nested browser iframe, matched to the parent post text.

Run

Local server: with Node.js installed, open a terminal in this folder and run:

npm start

Open http://127.0.0.1:4178/ for the homepage, or http://127.0.0.1:4178/scene/ for the full scene editor. WebGL must be enabled. No npm install is needed for this command. Press Ctrl+C to stop the server.

Drag to orbit, hold Shift while dragging to pan up/down/left/right, scroll or pinch to zoom, and use Run train / Pause train. Reset view returns to the saved default camera framing. You can zoom in well past a TV-filling close-up. The camera keeps the room's sides, top, and floor edges outside the entire view, including after panning, zooming, resizing, or importing. The scene's textures and geometry are generated by the recovered scene code. All pages use the logo's solid background color, initially #f3f0e6; the Cross-hatch effect draws its ink over that color without background images.

The tree is topped with a faceted golden five-pointed star. The starting view uses the camera captured from the local editor; scroll or pinch to explore the scene.

Project defaults were captured from the local logo and credenza editors on 2026-09-20. Fresh sessions use those colors, camera, effects, playback state, and panel visibility; the camera framing was later tightened around the television. Existing browser saves still take precedence. Reset controls restore the corresponding captured defaults.

Homepage preview

Open http://127.0.0.1:4178/ for the combined homepage. It uses the small Big Feeling logo, the live Christmas scene, and the same interactive PAD sphere as the editors. Navigation sits on the left on desktop and across the top on mobile. The sphere sits at the bottom of the desktop navigation, keeping a small margin from the window’s left edge and overlapping the sidebar’s right edge. The dropdown stays centered below the sphere. The sidebar divider runs from the image’s top to above the intensity ring, stopping short of the ring at the sidebar’s right edge. The dropdown and intensity-ring gap sit below the sphere. On mobile, the smaller selector sits near the top of the screen, centered over the navigation. A hamburger button opens the links, and two inset border segments leave clearance around the intensity ring. The Christmas image has no selector cutout, always uses a 16:9 aspect ratio, and shares the body text column’s 57ch maximum width. Desktop content keeps at least 24px of clearance from the intensity ring; mobile text scales modestly to retain the shorter book-like line length. Drag the Christmas image to orbit, Shift-drag to pan, and scroll or pinch to zoom, just as in the scene editor. With a mouse, the scene also makes a subtle, eased perspective shift toward the cursor and gently zooms in as the cursor approaches the image or out as it moves away. This presentation-only motion is not saved.

The Christmas presentation uses the captured scene defaults without modifying saved editor settings. The logo's sphere follows the selector's current front-facing color gradient live, before sepia and without mesh lines, points, or labels. The page favicon follows the same animated, sepia-treated sphere. When an emotion starts moving toward the reticle, the homepage body switches to its four-paragraph “Big Feeling copy” from the Emotion Cue Matrix while the television transitions between videos through animated analog broadcast static. Dragging the sphere raises static over the current picture; releasing it swaps the source under full static and reveals the new picture as the 420 ms globe snap finishes. Automatic and dropdown selections reach full static halfway through that same motion, switch sources, and then clear. Characters briefly scramble through a seeded set of unstable glyphs before progressively resolving into the new copy. Only new words or words whose paragraph-relative positions moved are decoded; unchanged words stay stable. Encoded words interpolate from their old lengths to their new lengths using only uppercase A–F and 1–9. The decode starts with the globe's 420 ms selection motion so both finish together. Reduced-motion preferences produce an instant swap instead. Its saved soft edge and cross-hatch settings then apply to that source; the logo retains its own sepia filter. Headings use locally bundled Literata; body copy and navigation use locally bundled Inter. On the homepage, the globe animates to a random emotion on load and chooses a new, non-repeating emotion every 6.5 seconds until the visitor interacts with the selector.

An italic Literata caption beneath the Christmas image reports the live pleasure, arousal, and dominance coordinates as 0–255 bytes. Beside them it shows the active five-bulb repeat beside a dmx512 label as an uppercase hexadecimal byte stream, including its leading 00 start code. The narrative row shows the .mp4 filename associated with the dropdown's live emotion—the closest landmark to the reticle while dragging—even before the television completes its transition. dimmer reports the dominance-driven lamp output from 0V~ to 120V~. A right-aligned countdown shows the time until the next automatic emotion and disappears permanently after a manual selector interaction stops autoplay.

The Christmas scene element exposes ambient-lighting methods after its data-ready attribute becomes true. Supply separate current and target states and animate mix from 0 to 1 to crossfade the hemisphere, key, and fill lights together:

const scene = document.getElementById('christmas-credenza-tight-3d');
scene.setAmbientLighting({
  current: scene.getAmbientLighting().current,
  target: {
    brightness: 1.25, temperature: 5200, ambientLevel: .8,
    shadowContrast: 1.3, shadowSoftness: 6,
    keyDirection: -10, keyElevation: 55, fillBalance: .7,
  },
  mix: 0,
});
scene.setAmbientLightingMix(.5);

brightness ranges from 0–4, temperature from 1000–12000 K, ambientLevel and fillBalance from 0–3, shadowContrast from 0–2, shadowSoftness from 0–12, keyDirection from −180–180°, and keyElevation from 5–85°. Values are clamped to these ranges. Partial current or target updates preserve their other values.

The Christmas Credenza toolbar's Background brightness slider coordinates those ambient fields with the plaster wall color. 50% preserves the authored scene. Toward Dark, the room becomes cool and low-fill with deeper shadows, leaving the warm lamp and Christmas tree as the dominant practical lights. Toward Bright, the wall, ambient light, and fill rise while shadows soften, ending at the appearance previously shown at 75%. It does not change the lamp, tree, TV, or renderer exposure. The scene element also exposes setBackgroundBrightness(value) and getBackgroundBrightness() using a clamped 0–1 value.

On the homepage, background brightness follows the selector's surface pleasure directly: P = −1 (0% pleasure) uses 10% on the scene brightness control, P = 0 uses 45%, and P = 1 (100% pleasure) uses the current 80% background setting. The intensity ring does not alter this surface-derived value. The selector publishes it as pad-selection-change.detail.backgroundBrightness.

The tree follows Emotion Cues!D2:D65. Each emotion supplies a repeating five-bulb DMX RGB sequence; the scene repeats it across all 155 bulbs and updates individual bulbs at randomized delays within a 180 ms window instead of crossfading the whole strand in sync. The scene element exposes setTreeLighting(sequence) and getTreeLighting() after data-ready is true.

The scene also exposes setLampLighting(brightness) and getLampLighting(). Brightness is clamped to 0–1 and linearly controls the lamp's point light, secondary wash, shade emission, and visible bulb. On the homepage this is fed directly from the selector's surface dominance: D = −1 becomes 0, D = 0 becomes 0.5, and D = 1 becomes 1. The intensity ring does not alter this surface-derived value. The selector publishes the same value as pad-selection-change.detail.brightness.

For live appearance controls, open http://127.0.0.1:4178/?debug=true with npm start, or open /?debug=true on the npm run dev server. Logo crosshatch and edge softness use the logo editor’s saved settings. Christmas crosshatch and CRT video settings save separately for the homepage. Changes update immediately and survive reloads, including visits without the debug parameter. Controls are unavailable in production builds.

The homepage Christmas crosshatch scale follows the rendered image width: it is 2.0 through 400 CSS pixels, interpolates linearly from 2.0 to 1.5 between 400 and 480 pixels, and remains 1.5 at desktop sizes. Device-pixel ratio does not change that visual scale.

Layout and placeholder copy: index.html and src/home.css. The legacy home/index.html route remains available, while the scene editor lives at scene/index.html.

Save and restore

Save JSON downloads christmas-credenza-settings.json. Import JSON restores a previously saved file. A snapshot includes:

  • Camera orbit, zoom, and pan target (together specifying its position and direction).
  • Background-brightness atmosphere and wall tone.
  • All CRT and Cross-hatch values and selected effect.
  • TV on/off state and playback position.
  • Train running/paused state, track position, and wheel rotation.
  • Which settings panels are open.

Settings also save automatically to a cookie for one year and restore on reload. Moving train/video positions are checkpointed every two seconds and saved when leaving or hiding the page. Playback resumes from the saved position; time spent away does not advance the scene. A local-storage backup supports browsers that block cookies. If browser storage is disabled, JSON export/import remains available. Restart an already-running npm start process after updating the app. The current server supports the video byte-range requests needed to restore playback positions.

Imports are validated before changing the scene; unsupported versions or malformed files show an error. Numeric values are limited to supported ranges, and camera positions are constrained to keep the room edges hidden. The JSON contains state, not the bundled video or textures.

Television video

The homepage television plays the video in content/emotions/ that matches the emotion selected by the PAD dropdown. Its source changes while fully hidden by analog static, which clears when the sphere finishes snapping to the new emotion. The scene editor chooses a random emotion video on each page load. All videos repeat with audio muted and volume set to zero. Playback continues independently of the train and works with Cross-hatch II enabled. Footage scales to fill the curved CRT glass, preserving its proportions and cropping the edges as needed. Each video frame is limited to 256 pixels on its longest side before being cropped to cover the screen. Smooth texture filtering gives the enlarged picture a softer CRT look. Smaller source videos keep their original resolution. Change MAX_VIDEO_TEXTURE_SIZE in src/tv-video.js to adjust this limit.

Open CRT settings in the toolbar to adjust the TV picture. The TV vignette slider adjusts edge and corner darkening live, from 0% (no vignette) to 100% (the strongest effect). It defaults to 70% and retains your setting when video is toggled off and back on. The slider is disabled while the video is off. The vignette's darkening is rendered at 62.5% of its original intensity without changing the size or shape of its falloff. The screen keeps its reflective clearcoat glass. A soft rectangular glow surrounds the screen, masked by the glass so it only appears outside the screen edge, and a rectangular area light illuminates nearby objects. The glow and light follow the average color/brightness of the displayed footage, sampled at most ten times per second. Reflections use the captured room environment.

Click TV video: On in the toolbar to turn the content off: playback pauses, the glow and screen light switch off, and the original screen texture and UV mapping return. Click TV video: Off to resume silent playback from the paused position.

The 64 emotion-to-video filename mappings mirror the filename column in the Christmas Credenza Emotion Cue Matrix. When the curated PAD set changes, update src/emotion-videos.js and add the matching browser-compatible MP4 to content/emotions/. The test suite checks that every dropdown value has a unique file. Reload the development page, or run npm run build for the packaged editions.

To reproduce the bundled compression with FFmpeg:

ffmpeg -i source.mp4 \
  -map 0:v:0 \
  -vf 'scale=256:256:force_original_aspect_ratio=decrease:force_divisible_by=2:flags=lanczos,setsar=1' \
  -c:v libx264 -preset slow -crf 28 -pix_fmt yuv420p \
  -an -map_metadata -1 -movflags +faststart content/emotions/emotion.mp4

The production build includes all 64 emotion videos as local assets. If browser policy blocks muted autoplay, clicking the scene starts playback. If the video cannot load, the original screen remains visible with a status message.

Logo editor

Open Logo editor from the scene toolbar, or go to /logo/. The editor adds the fixed Big Feeling wordmark and sphere layout from the supplied reference. Literata lettering is pre-outlined and cannot be edited. Adjust the sphere's three colors, direction, balance, highlight, and edge softness; the preview updates immediately. Colors support pickers and six-digit hex inputs, and every slider has a numeric input for exact values. Reset sphere restores the sphere and hatch defaults while preserving background and export preferences. Hatch controls appear when Apply to sphere is enabled. The default palette is #00e1ff, #5ab054, and #ff00dd, with direction 0°, balance 50%, highlight 40%, and edge softness 25%. Cross-hatch starts enabled with scale 0.12, thickness 1.4, and contour 3.96. Export defaults remain opaque at 2×. Saturation is fixed at 100%. Grain is always 0 with cross-hatch enabled and 8% for the smooth sphere, including when restoring older settings and rendering SVG or PNG exports.

The preview sphere alone has a CSS sepia(0.33) filter. The SVG wordmark and page remain unfiltered; this display filter is not baked into exported images.

Apply to sphere uses the same CrossHatchEffect class, GLSL shader, CMYK line angles, controls, and normal contours as the credenza. The logo opts into ink-only compositing: gaps have zero alpha and reveal the selected background (or transparency). There is no solid gradient or paper disc beneath the hatch strokes. The credenza composites the same ink over the solid page color. The sphere's edge softness reduces ink density before the hatch lines are generated, so the fade consists of thinning strokes and widening transparent gaps, including contour ink, rather than a smooth opacity fade over the strokes. Use saved credenza settings copies its last saved Cross-hatch II values into the logo editor. Logo settings save separately on this device.

Export SVG and Export PNG include the entire logo at 1×, 2×, or 3× size. Both support transparent backgrounds, including the gaps within the sphere. The SVG retains vector lettering and embeds a PNG of the shader-rendered sphere to preserve the exact effect; it does not require fonts or external images. Export resolution increases without changing hatch spacing. The preview always uses the page background; Transparent export omits it from downloaded files. Page background updates both editors, including another open credenza tab. The chosen color is saved with the logo settings.

npm start, Vite, and the GitHub Pages build all include the /logo/ route. Font licenses are in vendor/fonts/ and included in the production distribution.

PAD sphere

Open PAD sphere from either editor, or visit /pad/. This brings in the selector from the supplied pad-sphere-threejs project (found in ~/Downloads/), with a corrected YUV color mapping, 64 curated emotion landmarks, a selection marker, and intensity ring. It uses the existing bundled Three.js without new dependencies. The initial view uses the selected landmark's exact direction and intensity, centering its point under the reticle before the first frame.

Drag the colored triangle mesh to choose the Pleasure, Arousal, and Dominance direction, and drag the ring clockwise from low on the left to high on the right to change intensity. Its dot has a fixed 1px black outline and the page background as its fill. The ring itself is a 1px SVG stroke using the same color as the horizontal rules. The compact emotion dropdown is 114px wide and sits below the sphere in the inactive 60-degree bottom gap, with extra vertical space above it. The enlarged globe reduces the previous gap to the ring by about one-third. The dropdown's placement scales with the viewport. Releasing either drag eases rotation and intensity to the nearest emotion landmark over 420 ms; a new drag interrupts the snap. Cancelled drags, lost pointer capture, and window blur also finish by snapping; releasing outside the canvas cannot leave the globe between landmarks. Nearest emotions and labels use great-circle distance between directions on the globe, ignoring intensity. The destination then restores its recorded intensity. At zero intensity, snapping still uses the globe's retained direction. Reduced-motion preferences skip the animation. Double-click or choose Reset to neutral to set intensity to zero. With the sphere focused, arrow keys rotate, +/− change intensity, and Home returns to neutral. The whole ring stays visible on narrow screens.

The selector uses 40 positive, 11 negative, and 13 neutral/mixed moods, keeping roughly the requested 4:1 positive-to-negative mix across its vocabulary. Happy, Sad, Angry, Ennui, Fearful, Stoic, and Nostalgic are reserved. Fearful retains its original 1977 name and coordinates; it is not relabeled Afraid. The reviewed allowlist in src/pad-model.js excludes sexual terms, including Aroused. Mood categories and familiarity order are editorial choices, not classifications from the studies. The dropdown is alphabetical. Near-synonyms are consolidated, preferring the 1977 entry. Examples include Appreciative over Grateful/Thankful, Startled over Surprised/Astonished, Loved over Love/Affectionate, and Sleepy over Drowsy. Point tooltips identify the source year and term. Overwhelmed uses its 1977 values (0.14, 0.45, −0.24) to cover a less crowded direction. Loved fills a distinct direction without being reserved a slot.

Up to 20 points appear on both desktop and narrow screens, with labels only for the four nearest the reticle along the globe's surface during an active drag. Labels and their leader lines stay hidden on page load, while idle, and throughout snapping. Ending or cancelling a drag fades them out until the next drag. The dropdown continues to show the selected emotion, including after clicking a point or label or choosing an emotion directly. Once centered and at rest, the selected point expands to fill the reticle's interior with its measured color, without the point's white border or dark outline. The original bordered dot scales up and fades as a separate layer of color expands to fill the reticle over 240 ms. Dragging or snapping reverses that transition back to the normal dot. The fill tracks the reticle's projected size when resizing; reduced-motion preferences disable the scaling and fading animation. The four nearest points always get display slots. The selection and hovered point also remain visible when front-facing; hover shows a tooltip without replacing any of the four labels. Remaining points fill the largest angular gaps, preferring 1977 landmarks over supplemental terms when separation differs by at most three degrees, then using valence and familiarity order. Labels avoid the reticle, point markers, and one another, with leader lines when offset. Labels and their leader lines fade in and out with a 180 ms CSS opacity transition; reduced-motion preferences disable the fade.

The measured positions are preserved. The per-view mix follows the visible hemisphere rather than imposing quotas that would leave gaps; some views have fewer than 20 front-facing points. The vocabulary uses 45 original landmarks and 19 supplementary word ratings to improve angular coverage.

The 64-term set was recalculated using great-circle distances between normalized PAD vectors, excluding intensity. Selection reserves the seven required moods, fills larger surface gaps, avoids near-synonyms and crowded directions, and favors 1977 entries when coverage is comparable. Humorous, Sensitive, and Understanding add three verified supplemental directions.

Compared with the previous 64-term set, a check across 5,000 uniformly distributed directions reduces the largest sampled gap to a landmark from 36.4° to 33.2°, and the share of directions more than 30° from a landmark from 1.78% to 0.64%. The minimum separation between any two selected landmarks increases from 2.87° to 5.92°. All source coordinates and intensities remain unchanged.

Click a point or label, or use Explore emotions, to select any of the 64 exact landmarks, including its intensity. Snapping and emotion readouts use this same curated set. The points project PAD directions onto the surface; the ring retains the third dimension. The source data still preserves all 151 original terms.

The surface, reticle, and YUV color swatch update together throughout dragging and snapping. Each landmark dot uses its own measured PAD color. The swatch also shows the sRGB hex value. The sphere evaluates the mapping per pixel at the current intensity, avoiding interpolation of clipped vertex colors. The globe's color surface then receives a CSS sepia(0.5) filter. Mesh lines, reticle, intensity controls, landmark points, labels, and the YUV swatch remain unfiltered, so the swatch and numeric color still report the exact YUV mapping.

The dataset in src/pad-landmarks.js is transcribed from the Mean columns in Russell & Mehrabian (1977), Table 4, pp. 286–289, in original row order. It contains exactly 151 distinct terms and their reported P/A/D means on [-1, +1]; it does not substitute SDs, regression coefficients, or invented synonyms. Source: paper scan, DOI: 10.1016/0092-6566(77)90037-X.

Supplementary entries in src/pad-warriner.js come from Warriner, Kuperman & Brysbaert (2013), Norms of valence, arousal, and dominance for 13,915 English lemmas. They are affective word ratings, not additional 1977 emotion measurements. The file retains each original word, CSV ID, and aggregate V.Mean.Sum, A.Mean.Sum, and D.Mean.Sum from the publisher's BRM-emot-submit.csv supplement. Each 1–9 mean becomes (mean − 5) / 4 on [-1, +1], with valence used as pleasure. For example, Content's means (6.70, 3.17, 5.92) map to PAD (0.425, −0.4575, 0.23). The file preserves the 35 imported rows for provenance; only 19 distinct supplemental concepts remain selectable. Existing 1977 terms keep their original values and names. Sources: paper, publisher data.

The app's color mapping uses D → Y, P → U (inverted), A → V, retaining the supplied project's video-range coefficients:

y = 127.5 × (D + 1) − 16
u = 127.5 × (1 − P) − 128
v = 127.5 × (A + 1) − 128
R = 1.164y + 1.596v
G = 1.164y − 0.392u − 0.813v
B = 1.164y + 2.017u

RGB is clipped to 0–255 and treated as display sRGB, without a second gamma encoding. The same matrix generates both the JavaScript mapping and sphere shader. For example, Table 4's Happy maps to #FFBA00, Angry to #FF51FF, and Sad to #194EF2. Color is an app visualization, not a measurement supplied by the paper. The supplied generated “152 emotions” image contains duplicated/extra terms and swatches that do not match the formula above at the published PAD coordinates. Its axis labels alone do not specify the conversion matrix and scaling, and the PNG does not contain the underlying numerical mapping. The app retains the explicit formula rather than adjusting values or colors to fit the image. For Angry, raw RGB (286.150, 80.843, 297.035) is clipped to (255, 81, 255); clipping cannot turn that magenta result into the atlas's red swatch.

Landmark dots use their own exact PAD values. The surface, reticle, and swatch use the current direction and intensity, so their colors can vary while the nearest-emotion readout still shows the same name. Selecting a landmark or finishing a snap restores its exact coordinates and matching color at the reticle. Clipping can make different PAD values share the same saturated RGB color.

Typography and text colors match the logo editor. The transparent canvas reveals the shared page background, including changes made in another open logo tab; the ring and knob also follow that palette. The source's dark backdrop is removed. npm start, Vite, and the production build include the route.

Cross-hatch post-processing

Choose Post-processing → Cross-hatch II in the toolbar above the scene to apply spite's effect. The configuration panel appears beside the scene on desktop and below it on smaller screens. The toolbar stays visible while scrolling, and the scene keeps its 3:2 aspect ratio while fitting the window height. Every change redraws immediately, including while the train is paused. Choose None (original) to return to the normal view. Effect settings are retained when switching modes and across reloads.

Control Range / choices Default
Scale 0.1–2 1.5
Thickness 0–3 1
Contour 0–10; 0 disables outlines 0.5
Edge fade (Christmas scene) 0–50%; 0 disables the fade 2%

Cyan, Magenta, and Yellow weights are fixed at 1, Black at 0, and ink color at its black default in both editors, including when restoring older saved settings. Black and ink color have no editable controls.

The table lists the credenza defaults; the logo uses scale 0.12, thickness 1.4, and contour 3.96. Reset effect restores each editor's captured defaults. The effect uses the existing scene's colors and lighting with CMYK lines and normal-based contours. The demo's separate sample-object, material, and environment controls do not apply to this scene. The Christmas tree's foliage, needles, trunk, lights, and wire are masked from contours while keeping their colored hatching and glow. The metal train rails are also masked. The wooden track slats, star, tree base, and rest of the scene still respond to the Contour slider. The effect uses the solid page color; no paper images are loaded or embedded. At all four scene edges, ink density tapers into the page background before hatch strokes are generated, using the same approach as the logo's soft sphere edge. This also tapers contour ink and softens the corners of the scene. Adjust Edge fade to change the width of the fade as a percentage of the scene's shorter side. It saves with the scene and returns to 2% with Reset effect. Older saved settings still load, with their obsolete paper selection ignored. Enabling the effect adds a color pass, a mesh-normal pass, and a full-screen composite; animation may run slower on less powerful graphics hardware. Normal mode bypasses these passes.

CRT shader and settings panels

The TV uses an adaptation of gingerbeardman's Serenity Shader. It processes only the TV picture and works together with the whole-scene Cross-hatch effect. The 256 px source limit, silent looping playback, glass reflections, and rectangular TV light are retained.

Use CRT settings and Cross-hatch settings to open or close their panels. Each panel also has a Close button. Closing a panel keeps its effect and values; settings and panel visibility persist across reloads. Panels sit beside the scene on desktop and below it on small screens. Opening a panel brings it into view.

CRT control Range Default
Scanline strength 0–100% 33%
Scanline count 50–1200 144
Adaptive strength 0–100% 50%
Scanline offset 0–1 0
Brightness, contrast 0.6–1.8 1.75, 0.89
Saturation 0–2 0.81
RGB shift 0–1 0.1
TV vignette 0–100% 70%
Curvature 0–0.5 0
Flicker 0–15% 3%
Analog static 0–100% 0%
Bloom strength 0–1.5 0.65
Bloom threshold 0–1 0.19

Analog static mixes a procedural monochrome broadcast-noise shader over the picture; at 100% the source video is completely obscured. The homepage drives a separate transition mix so this authored editor value remains saved and reusable. Enable CRT picture effect bypasses or restores the picture filters without stopping the video. Reset CRT restores the table's defaults and enables the picture effect. Turning TV video off disables these controls, pauses playback, restores the original texture, and switches off the light and bloom.

The reference's internal bloom is adapted to the existing external halo so bloom stays outside the glass. The existing adjustable vignette is used once. Scanlines fade to their average when too small to display cleanly, reducing moire during camera movement. See vendor/crt/README.md and LICENSE.txt for source attribution.

Optional Vite workflow

Use Node.js 24 (recorded in .nvmrc). Installation requires internet access; the app itself does not.

npm install
npm run dev

To rebuild for a static web host:

npm run build
npm run preview

The production build is generated in dist/, which is gitignored. Upload the contents of dist/ to a static host after building. Build output is left unminified for inspection. The official WebAssembly compiler fallback is included in the development dependencies for systems where Vite's native compiler is unavailable.

Deploy to GitHub Pages

The workflow in .github/workflows/pages.yml follows the same build/deploy setup as ~/matthew-cook-static: Node.js 24, npm ci, a build job that uploads dist/, and a deployment job using the github-pages environment. It also runs npm test before building. Pushes to main deploy automatically; Build and deploy GitHub Pages can also be run manually from the Actions tab.

  1. Commit and push the source changes, including .github/workflows/pages.yml, .nvmrc, package-lock.json, and content/emotions/.
  2. In the repository's Settings → Pages → Build and deployment, set Source to GitHub Actions.
  3. Push to main, or run the workflow manually.

The production site is https://bigfeel.ing/. The workflow builds every asset from / and includes public/CNAME, so JavaScript, CSS, fonts, textures, and videos resolve against that custom-domain root. No deploy token or additional secret is needed; the workflow uses GITHUB_TOKEN.

The workflow rebuilds from source on every run. Its artifact contains the scene, logo editor, PAD sphere, video, and license notices from dist/. Running npm run build produces the root-relative custom-domain artifact. To exercise a repository-subpath build separately, set BASE_PATH explicitly.

See GitHub's custom Pages workflow documentation.

Files

  • index.html, src/, vendor/ — homepage and readable local dependencies.
  • scene/index.html — full Christmas scene editor.
  • src/cross-hatch.js, src/post-processing.js — optional effect and configuration controls.
  • src/tv-video.js, src/crt-screen.js, src/tv-static-transition.js — silent looping video, analog-static transitions, reflective CRT glass, glow, area light, and on/off control.
  • src/crt-shader.js, src/crt-controls.js, src/effect-panels.js — CRT picture effects, live controls, and reopenable settings panels.
  • src/camera-controls.js, src/scene-state.js — constrained orbit/pan/zoom, JSON import/export, and automatic state persistence.
  • pad/index.html, src/pad-editor.js, src/pad-model.js — PAD sphere selector, shared palette, and YUV mapping.
  • src/pad-landmarks.js — all 151 Table 4 mean PAD triplets, with source attribution and original row order.
  • .github/workflows/pages.yml, public/CNAME, .nvmrc — custom-domain GitHub Pages deployment and Node.js version.
  • original/scene.js — unmodified module text recovered from the requested post.
  • original/rendered-app.html — archived live iframe DOM, including the original host wrapper. This is a source record and retains its original external URLs; use the runnable files above.
  • original/preview.png — the original scene captured in the conversation.
  • EXTRACTION.json — source identification, SHA-256 hashes, and packaging changes.
  • dist/ — generated Vite application (gitignored).

Original extraction and subsequent changes

The initial extraction replaced the Three.js 0.160.1 CDN import with ../vendor/three.module.js. The app now also routes drawing and resize events through optional post-processing and adds its controls. A faceted gold star and mounting stem top the tree, and the initial camera view is wider to include them. The existing scene geometry, seed, generated textures, lighting, bulb wrapping, and train logic are preserved. original/scene.js remains the unmodified source record.

The page wrapper uses the captured styles and a local copy of the original sandbox stylesheet. It removes the already-rendered canvas so the original code can create it again, resets the loading state, omits unused host helpers, and permits normal page scrolling. The scene keeps its original 3:2 aspect ratio.

original/rendered-app.html is a serialization of the live DOM, not a claim to the pre-render server file bytes. The scene module itself is preserved exactly as read from its script element.

Effect verification

  • npm test: regression tests cover render-state restoration (including errors), resizing, parameter limits, shared background colors, and the TV light, bloom, original-screen restoration, CRT parameter limits, and retained shader settings. Camera checks cover Shift-drag routing and room coverage across 500 combinations of orbit, pan, zoom, import, and resize. State checks cover JSON/cookie round trips, invalid imports, range normalization, load restoration, and page-exit saving.
  • npm run build: Vite production build passes.
  • Chrome: effect toggling, all slider limits, reset, retained settings, and train animation verified; the production build rendered without shader or console errors.
  • The production view and controls fit a 390 px viewport without horizontal overflow.

Initial extraction verification

  • Original and packaged scene modules match after reversing the single import replacement.
  • Chrome rendered the source app and Vite production build without console errors.
  • Run/Pause changed the control state and visibly moved the train.
  • JavaScript syntax checks and Vite production build passed.

Third-party components

Three.js 0.160.1 is bundled under MIT; see vendor/THREE-LICENSE.txt. Captured Tailwind CSS 3.4.17 styles are covered by vendor/TAILWIND-LICENSE.txt. Vite's license is included in vendor/VITE-LICENSE.md. No external fonts, images, or 3D models are required.

Serenity Shader by Matt Sephton (@gingerbeardman) is adapted under MIT; see vendor/crt/LICENSE.txt. Its license and attribution are included in the production build.

Cross-hatch II comes from spite/sketch under MIT; see vendor/cross-hatch/LICENSE.txt and vendor/cross-hatch/README.md for attribution and integration details. Production output includes licenses in dist/licenses/.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages