{"id":408,"date":"2026-11-01T13:10:53","date_gmt":"2026-11-01T12:10:53","guid":{"rendered":"https:\/\/npham.dk\/?p=408"},"modified":"2026-11-01T13:10:54","modified_gmt":"2026-11-01T12:10:54","slug":"halloween-projection-mapping","status":"publish","type":"post","link":"https:\/\/npham.dk\/?p=408","title":{"rendered":"Halloween Projection Mapping"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">After a long break from blogging, I finally have a project worth sharing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the last few Halloweens, I&#8217;ve been projecting spooky videos onto my front door. The setup worked reasonably well, but aligning the projection was always a manual process. Every year I would spend time moving the projector, adjusting angles, and tweaking the image to get it to roughly fit the shape of the door.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It never quite lined up perfectly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At the same time, I wanted to make the experience more interactive. Instead of just looping a video all evening, I wanted a jump-scare effect that could be triggered on demand when visitors approached the house.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since I already run Home Assistant and have motion sensors and Zigbee buttons installed around the house, it felt natural to integrate those into the setup as well.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That combination of requirements led me to build my own projection mapping tool.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The goals were simple:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Make it easy to align videos to the front door.<\/li>\n\n\n\n<li>Play a looping background video.<\/li>\n\n\n\n<li>Trigger jump-scare videos manually or automatically.<\/li>\n\n\n\n<li>Integrate with Home Assistant motion sensors and Zigbee buttons.<\/li>\n\n\n\n<li>Run entirely in a browser without additional software.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The result is a browser-based projection mapping application that allows videos to be warped and aligned to almost any rectangular surface using an adjustable four-corner mapping grid.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The project is available on GitHub:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/github.com\/kim82\/ProjectionMapping\" target=\"_blank\" rel=\"noopener\">Projection Mapping on GitHub<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why a Browser?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I wanted something lightweight that could run on almost any machine without installation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The entire application is written using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>HTML<\/li>\n\n\n\n<li>CSS<\/li>\n\n\n\n<li>JavaScript<\/li>\n\n\n\n<li>WebGL<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">There is no backend, database, or build process. Running the application is as simple as starting a local web server and opening a browser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One browser window acts as the control panel, while a second window runs fullscreen on the projector output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Controller and output communicate directly through the browser using <code>BroadcastChannel<\/code>, and all settings are stored locally using <code>localStorage<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Projection Mapping<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The core feature is perspective correction.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With the projection mapping tool, alignment becomes much easier. Instead of moving the projector around, I can simply drag four corner points until the projected image matches the physical surface. The mapping is saved automatically, making future setup much faster.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The mapping itself is rendered using WebGL, allowing the video to be warped to fit the target surface while maintaining smooth playback.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A built-in test pattern helps with calibration before switching to video playback.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/IMG_2392_still.jpeg\" data-src=\"https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/IMG_2392_still.jpeg\" data-thumb=\"https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/IMG_2392_still.jpeg\" class=\"ml-lightbox-enabled\"><img loading=\"lazy\" decoding=\"async\" width=\"552\" height=\"310\" src=\"https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/IMG_2392_still.jpeg\" alt=\"\" class=\"wp-image-411\" srcset=\"https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/IMG_2392_still.jpeg 552w, https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/IMG_2392_still-300x168.jpeg 300w\" sizes=\"auto, (max-width: 552px) 100vw, 552px\" \/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Interactive Video Playback<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The application supports two different video types:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Main Video<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The main video can either loop continuously or remain idle until triggered by a sensor.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Trigger Video<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A separate trigger video can be launched manually or automatically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This makes it possible to create jump scares on demand. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Home Assistant Integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Previously, the projection would simply run in a loop throughout the evening. This worked, but it meant the effect was often missed, and the jump scares quickly became predictable.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Since I already use Home Assistant for much of my home automation, I decided to integrate the projection system directly with it, so I could reuse some of my sensors and buttons.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The application connects to Home Assistant through its WebSocket API and can react to sensor events in real time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This makes it possible to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Trigger jump-scare videos with a zigbee button when someone knocks on the door.<\/li>\n\n\n\n<li>Keep the projection idle until visitors arrive, and only play when someone comes close to the house.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-video\"><video height=\"1080\" style=\"aspect-ratio: 1920 \/ 1080;\" width=\"1920\" controls src=\"https:\/\/npham.dk\/wp-content\/uploads\/2026\/11\/jumpscare-button-trigger.mp4\"><\/video><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Disclosure<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">One thing worth mentioning is how this project was built.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Large parts of the code were developed with the assistance of AI tools. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>After a long break from blogging, I finally have a project worth sharing. For the last few Halloweens, I&#8217;ve been projecting spooky videos onto my front door. The setup worked reasonably well, but aligning the projection was always a manual process. Every year I would spend time moving the projector, adjusting angles, and tweaking the [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[24,17],"class_list":["post-408","post","type-post","status-publish","format-standard","hentry","category-none","tag-halloween","tag-home-assistant"],"_links":{"self":[{"href":"https:\/\/npham.dk\/index.php?rest_route=\/wp\/v2\/posts\/408","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/npham.dk\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/npham.dk\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/npham.dk\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/npham.dk\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=408"}],"version-history":[{"count":8,"href":"https:\/\/npham.dk\/index.php?rest_route=\/wp\/v2\/posts\/408\/revisions"}],"predecessor-version":[{"id":423,"href":"https:\/\/npham.dk\/index.php?rest_route=\/wp\/v2\/posts\/408\/revisions\/423"}],"wp:attachment":[{"href":"https:\/\/npham.dk\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=408"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/npham.dk\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=408"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/npham.dk\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=408"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}