<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Kasper Kamperman</title>
	<atom:link href="https://www.kasperkamperman.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.kasperkamperman.com/</link>
	<description>Creative Developer</description>
	<lastBuildDate>Sun, 26 Mar 2023 19:20:14 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
	<item>
		<title>Student evaluation of teaching</title>
		<link>https://www.kasperkamperman.com/edu/student-evaluation-of-teaching/</link>
					<comments>https://www.kasperkamperman.com/edu/student-evaluation-of-teaching/#respond</comments>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Mon, 07 Nov 2022 12:29:02 +0000</pubDate>
				<category><![CDATA[Edulog]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=3258</guid>

					<description><![CDATA[<p>As a teacher, I highly value the feedback I get from students. It helps me to evaluate my teaching and learn what students like and dislike. In the last years, I explored and iterated upon a method to enable the student evaluation of teaching. Course evaluation Our program, Creative Media &#38; Game Technologies, teaches new ... <a title="Student evaluation of teaching" class="read-more" href="https://www.kasperkamperman.com/edu/student-evaluation-of-teaching/" aria-label="More on Student evaluation of teaching">Read more</a></p>
<p>The post <a href="https://www.kasperkamperman.com/edu/student-evaluation-of-teaching/">Student evaluation of teaching</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>As a teacher, I highly value the feedback I get from students. It helps me to evaluate my teaching and learn what students like and dislike. In the last years, I explored and iterated upon a method to enable the student evaluation of teaching.</p>



<span id="more-3258"></span>



<h2 class="wp-block-heading">Course evaluation</h2>



<p>Our program, Creative Media &amp; Game Technologies, teaches new courses each quarter. A period is typically ten weeks long. Those courses get evaluated in the last week before the course ends and typically ask students about their experience of the course. How well were they prepared, and how would they grade the course? They also have the option to answer some open questions to leave their remarks. </p>



<p>As a teacher, I always found the &#8220;open question&#8221; remarks the most valuable. They are interesting to read and give a good insight into the student experience. The most helpful comments are from highly critical students who also think along and come up with suggestions for improvement. </p>



<p>One evaluation per period seemed to me like a missed opportunity. I wanted to evaluate more often to make quicker iterations, even during a course, next to the default course evaluation. To prevent evaluation fatigue, I kept the evaluations plain and straightforward. </p>



<h2 class="wp-block-heading">Student evaluation of teaching lab classes (version 1)</h2>



<p>Since open questions gave me the most valuable input, I started with three questions. I took inspiration from the Retrospective method used in Eduscrum. </p>



<ul><li>What went well?</li><li>What could be improved?</li><li>What <span style="text-decoration: underline" class="underline">must</span> be improved?</li></ul>



<p>At the end of each class, I handed out some post-its and asked them to answer these questions. The questions were summarized by using a simple emoji and the terms: Good, Improve, and Annoying. </p>



<p>Students could paste their post-its with their remarks under those three &#8220;categories.&#8221; I didn&#8217;t explicitly ask students to evaluate my teaching. The goal was to assess the whole class experience. </p>



<figure class="wp-block-image size-full"><img fetchpriority="high" decoding="async" width="1280" height="375" src="https://cdn.kasperkamperman.com/media//retro-post-its.jpg" alt="Post-it's showing emoijs with the terms Good, Improve and Annoying." class="wp-image-3260" srcset="https://kasperkamperman.com/media/retro-post-its.jpg 1280w, https://kasperkamperman.com/media/retro-post-its-480x141.jpg 480w, https://kasperkamperman.com/media/retro-post-its-800x234.jpg 800w, https://kasperkamperman.com/media/retro-post-its-768x225.jpg 768w" sizes="(max-width: 1280px) 100vw, 1280px" /><figcaption>Good :-), Improve :-|, Annoying :-(</figcaption></figure>



<figure class="wp-block-image size-full"><img decoding="async" width="1200" height="516" src="https://cdn.kasperkamperman.com/media//retro-post-its-good-4.jpg" alt="Results of the student evaluation." class="wp-image-3268" srcset="https://kasperkamperman.com/media/retro-post-its-good-4.jpg 1200w, https://kasperkamperman.com/media/retro-post-its-good-4-480x206.jpg 480w, https://kasperkamperman.com/media/retro-post-its-good-4-800x344.jpg 800w, https://kasperkamperman.com/media/retro-post-its-good-4-768x330.jpg 768w" sizes="(max-width: 1200px) 100vw, 1200px" /><figcaption>Student evaluation of teaching &#8211; Good category</figcaption></figure>



<p>The categories help to set priorities. At first, I hesitated to include the &#8220;Good&#8221; category because I was mainly interested in what didn&#8217;t work. However, the &#8220;Good&#8221; category helped me become aware of what works and what students actually appreciate. </p>



<p>After class, I would group the post-its with similar feedback with the affinity map method. </p>



<h2 class="wp-block-heading">Student evaluation of teaching lectures (during the pandemic). </h2>



<p>During the pandemic, we had to switch to online lectures. Which, in general, meant that I was doing my class in front of a screen without no way to check how the students received the information. Of course, I built in interaction moments with polls and audio chat, but it was mainly one-way communication. </p>



<p>I found it important to give students a way to leave their feedback on a lecture and evaluate my teaching. With the help of their feedback, I was also better able to iterate and adapt my lessons to a new way of teaching. </p>



<figure class="wp-block-image size-full"><img decoding="async" width="1200" height="630" src="https://cdn.kasperkamperman.com/media//evaluation_online-1.jpg" alt="First student evaluation of teaching slide with 3 questions. &quot;What did you like?&quot;, &quot;What could be better?&quot;, &quot;What was annoying to you?&quot;" class="wp-image-3270" srcset="https://kasperkamperman.com/media/evaluation_online-1.jpg 1200w, https://kasperkamperman.com/media/evaluation_online-1-480x252.jpg 480w, https://kasperkamperman.com/media/evaluation_online-1-800x420.jpg 800w, https://kasperkamperman.com/media/evaluation_online-1-768x403.jpg 768w" sizes="(max-width: 1200px) 100vw, 1200px" /><figcaption>Student evaluation of teaching presentation slide (1st version) (redacted link to an online form). </figcaption></figure>



<p>Through the slide above, I asked them to evaluate my teaching. I shared the link in the online chat environment. In the form, they could answer the three open questions shown above.</p>



<p>About 40% of the students used the form and left their feedback. </p>



<h2 class="wp-block-heading">Retrospective</h2>



<p>Students gave valuable feedback to improve teaching. However, some things are challenging to improve directly in the course. Other things might be out of my direct control. </p>



<p>Students also have different opinions. Some like the lecture pacing, while others mention that the pacing could be better. I wanted to share these insights with students and elaborate on my choices. An essential part of teaching is setting the right expectations. Doing a retrospective gave me the ability to make students part of course development and clarify the choices I&#8217;ve made. </p>



<p>For each lecture, I do a quick recapture of the previous class. After that, I would show a slide with (a summary) of the student&#8217;s feedback. During this retrospective, I take a short moment to talk about the input. </p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1200" height="630" src="https://cdn.kasperkamperman.com/media//retrospective_online.jpg" alt="Slide with feedback text I received from my students. Organised in three categories. " class="wp-image-3272" srcset="https://kasperkamperman.com/media/retrospective_online.jpg 1200w, https://kasperkamperman.com/media/retrospective_online-480x252.jpg 480w, https://kasperkamperman.com/media/retrospective_online-800x420.jpg 800w, https://kasperkamperman.com/media/retrospective_online-768x403.jpg 768w" sizes="(max-width: 1200px) 100vw, 1200px" /><figcaption>Retrospective Slide, shown at the start of the following lecture</figcaption></figure>



<p>Students valued the retrospective (which came back several times in the following &#8220;what did you like?&#8221; evaluation). And it helps to elaborate on choices like lecture pacing and length. </p>



<h2 class="wp-block-heading">Student evaluation of teaching lab classes (version  2) </h2>



<p>The questions &#8220;What could be better?&#8221; and &#8220;What was annoying to you?&#8221; determine the feedback&#8217;s priority. The feedback that students mark as highly annoying has a higher priority. After several evaluations, I figured I could remove the &#8220;annoying&#8221; question. Removing this question makes the evaluation faster and simpler for the students and me. </p>



<ul><li>Most students weren&#8217;t filling in the annoying question at all.  </li><li>Different students have different priorities. So what one student would put under &#8220;better&#8221; would be &#8220;annoying&#8221; for the other students. </li><li>The priority between &#8220;better&#8221; and &#8220;annoying&#8221; gave a bit more information, but I couldn&#8217;t act differently on this. In reality, I valued &#8220;better&#8221; responses as much as &#8220;annoying.&#8221; </li><li>Simpler is better. Why cause more cognitive strain if it&#8217;s not needed?</li><li>I found the annoying question and emoji to be too negative. </li></ul>



<p>I eliminated the &#8220;annoying&#8221; question and changed the &#8220;subtitle&#8221; of the &#8220;What could be better?&#8221; question to &#8220;Ideas for improvement.&#8221; This invites students to not only mention what could be better but also come up with ideas to improve it. </p>



<p>This change to two categories also made the retrospective more straightforward. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="352" src="https://cdn.kasperkamperman.com/media//evaluation_online_2-1-800x352.jpg" alt="Updated student evaluation of teaching slide with only 2 questions. &quot;What did you like?&quot;, &quot;What could be better?&quot;" class="wp-image-3276" srcset="https://kasperkamperman.com/media/evaluation_online_2-1-800x352.jpg 800w, https://kasperkamperman.com/media/evaluation_online_2-1-480x211.jpg 480w, https://kasperkamperman.com/media/evaluation_online_2-1-768x338.jpg 768w, https://kasperkamperman.com/media/evaluation_online_2-1.jpg 1200w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>Updated slide for the evaluation of teaching</figcaption></figure>



<p>For practical reasons, I still teach lectures online. The practical classes are at our location again. I decided to keep the class evaluation digital and not move back to post-its. </p>



<ul><li>Everyone is now used to scanning a QR Code with their smartphone. </li><li>Using a digital form for evaluation makes I can use the results in the retrospective slides easier. </li><li>Archiving the feedback is more manageable (no need to take photos and digitalize them myself). </li></ul>



<h2 class="wp-block-heading">Tops &amp; Tips</h2>



<p>The current evaluation method is similar to another feedback method called &#8220;Tops &amp; Tips.&#8221; </p>



<ul><li>Tops are the points that you did well. </li><li>Tips are the recommendations for where you can improve. </li></ul>



<p>While I don&#8217;t get insecure by straightly negative feedback, this is not the case for everyone. I am very critical (a remark I heard from students and colleagues), and sometimes I forget to mention the good things. This evaluation and feedback methodology nudges you to say positive things too.</p>



<p>I like this focus on positive feedback because it allows you to double down on the things that are going fine instead of solely focusing on things that need improvement. </p>



<h2 class="wp-block-heading">Next step</h2>



<p>Everyone who has to teach, lecture, or present can benefit from this evaluation methodology. That&#8217;s why I&#8217;m creating <a href="https://tiptopfeedback.com">a simple tool to speed up the evaluation and feedback process. </a></p>



<p>Please subscribe to my newsletter if you are interested in this journey. I only send out a newsletter when I have some serious updates.  </p>



<style type="text/css">@import url("https://assets.mlcdn.com/fonts.css?version=1667227");</style>
    <style type="text/css">
    /* LOADER */
    .ml-form-embedSubmitLoad {
      display: inline-block;
      width: 20px;
      height: 20px;
    }

    .g-recaptcha {
    transform: scale(1);
    -webkit-transform: scale(1);
    transform-origin: 0 0;
    -webkit-transform-origin: 0 0;
    height: ;
    }

    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0,0,0,0);
      border: 0;
    }

    .ml-form-embedSubmitLoad:after {
      content: " ";
      display: block;
      width: 11px;
      height: 11px;
      margin: 1px;
      border-radius: 50%;
      border: 4px solid #fff;
    border-color: #ffffff #ffffff #ffffff transparent;
    animation: ml-form-embedSubmitLoad 1.2s linear infinite;
    }
    @keyframes ml-form-embedSubmitLoad {
      0% {
      transform: rotate(0deg);
      }
      100% {
      transform: rotate(360deg);
      }
    }
      #mlb2-2052560.ml-form-embedContainer {
        box-sizing: border-box;
        display: table;
        margin: 0 auto;
        position: static;
        width: 100% !important;
      }
      #mlb2-2052560.ml-form-embedContainer h4,
      #mlb2-2052560.ml-form-embedContainer p,
      #mlb2-2052560.ml-form-embedContainer span,
      #mlb2-2052560.ml-form-embedContainer button {
        text-transform: none !important;
        letter-spacing: normal !important;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper {
        background-color: #f6f6f6;
        
        border-width: 0px;
        border-color: transparent;
        border-radius: 4px;
        border-style: solid;
        box-sizing: border-box;
        display: inline-block !important;
        margin: 0;
        padding: 0;
        position: relative;
              }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper.embedPopup,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper.embedDefault { width: 400px; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper.embedForm { max-width: 400px; width: 100%; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-align-left { text-align: left; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-align-center { text-align: center; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-align-default { display: table-cell !important; vertical-align: middle !important; text-align: center !important; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-align-right { text-align: right; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedHeader img {
        border-top-left-radius: 4px;
        border-top-right-radius: 4px;
        height: auto;
        margin: 0 auto !important;
        max-width: 100%;
        width: undefinedpx;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody {
        padding: 20px 20px 0 20px;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody.ml-form-embedBodyHorizontal {
        padding-bottom: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent {
        text-align: left;
        margin: 0 0 20px 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent h4,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent h4 {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 30px;
        font-weight: 400;
        margin: 0 0 10px 0;
        text-align: left;
        word-break: break-word;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent p,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent p {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px;
        font-weight: 400;
        line-height: 20px;
        margin: 0 0 10px 0;
        text-align: left;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent ul,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent ol,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent ul,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent ol {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent ol ol,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent ol ol {
        list-style-type: lower-alpha;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent ol ol ol,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent ol ol ol {
        list-style-type: lower-roman;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent p a,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent p a {
        color: #000000;
        text-decoration: underline;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-block-form .ml-field-group {
        text-align: left!important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-block-form .ml-field-group label {
        margin-bottom: 5px;
        color: #333333;
        font-size: 14px;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-weight: bold; font-style: normal; text-decoration: none;;
        display: inline-block;
        line-height: 20px;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedContent p:last-child,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-successBody .ml-form-successContent p:last-child {
        margin: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody form {
        margin: 0;
        width: 100%;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-formContent,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow {
        margin: 0 0 20px 0;
        width: 100%;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow {
        float: left;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-formContent.horozintalForm {
        margin: 0;
        padding: 0 0 20px 0;
        width: 100%;
        height: auto;
        float: left;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow {
        margin: 0 0 10px 0;
        width: 100%;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow.ml-last-item {
        margin: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow.ml-formfieldHorizintal {
        margin: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow input {
        background-color: #ffffff !important;
        color: #333333 !important;
        border-color: #cccccc;
        border-radius: 4px !important;
        border-style: solid !important;
        border-width: 1px !important;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px !important;
        height: auto;
        line-height: 21px !important;
        margin-bottom: 0;
        margin-top: 0;
        margin-left: 0;
        margin-right: 0;
        padding: 10px 10px !important;
        width: 100% !important;
        box-sizing: border-box !important;
        max-width: 100% !important;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow input::-webkit-input-placeholder,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow input::-webkit-input-placeholder { color: #333333; }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow input::-moz-placeholder,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow input::-moz-placeholder { color: #333333; }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow input:-ms-input-placeholder,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow input:-ms-input-placeholder { color: #333333; }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow input:-moz-placeholder,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow input:-moz-placeholder { color: #333333; }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow textarea, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow textarea {
        background-color: #ffffff !important;
        color: #333333 !important;
        border-color: #cccccc;
        border-radius: 4px !important;
        border-style: solid !important;
        border-width: 1px !important;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px !important;
        height: auto;
        line-height: 21px !important;
        margin-bottom: 0;
        margin-top: 0;
        padding: 10px 10px !important;
        width: 100% !important;
        box-sizing: border-box !important;
        max-width: 100% !important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-radio .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::before {
          border-color: #cccccc!important;
          background-color: #ffffff!important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow input.custom-control-input[type="checkbox"]{
        box-sizing: border-box;
        padding: 0;
        position: absolute;
        z-index: -1;
        opacity: 0;
        margin-top: 5px;
        margin-left: -1.5rem;
        overflow: visible;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::before {
        border-radius: 4px!important;
      }


      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow input[type=checkbox]:checked~.label-description::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox input[type=checkbox]:checked~.label-description::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-input:checked~.custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-input:checked~.custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox input[type=checkbox]:checked~.label-description::after {
        background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-input:checked~.custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-input:checked~.custom-control-label::after {
        background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-input:checked~.custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-radio .custom-control-input:checked~.custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-input:checked~.custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-input:checked~.custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox input[type=checkbox]:checked~.label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox input[type=checkbox]:checked~.label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow input[type=checkbox]:checked~.label-description::before  {
          border-color: #000000!important;
          background-color: #000000!important;
          color: #ffffff!important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-radio .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-radio .custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-label::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-label::after {
           top: 2px;
           box-sizing: border-box;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::after {
           top: 0px!important;
           box-sizing: border-box!important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::after {
        top: 0px!important;
           box-sizing: border-box!important;
      }

       #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox .label-description::after {
            top: 0px!important;
            box-sizing: border-box!important;
            position: absolute;
            left: -1.5rem;
            display: block;
            width: 1rem;
            height: 1rem;
            content: "";
       }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox .label-description::before {
        top: 0px!important;
        box-sizing: border-box!important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .custom-control-label::before {
          position: absolute;
          top: 4px;
          left: -1.5rem;
          display: block;
          width: 16px;
          height: 16px;
          pointer-events: none;
          content: "";
          background-color: #ffffff;
          border: #adb5bd solid 1px;
          border-radius: 50%;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .custom-control-label::after {
          position: absolute;
          top: 2px!important;
          left: -1.5rem;
          display: block;
          width: 1rem;
          height: 1rem;
          content: "";
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox .label-description::before, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::before {
          position: absolute;
          top: 4px;
          left: -1.5rem;
          display: block;
          width: 16px;
          height: 16px;
          pointer-events: none;
          content: "";
          background-color: #ffffff;
          border: #adb5bd solid 1px;
          border-radius: 50%;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::after {
          position: absolute;
          top: 0px!important;
          left: -1.5rem;
          display: block;
          width: 1rem;
          height: 1rem;
          content: "";
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::after {
          position: absolute;
          top: 0px!important;
          left: -1.5rem;
          display: block;
          width: 1rem;
          height: 1rem;
          content: "";
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .custom-radio .custom-control-label::after {
          background: no-repeat 50%/50% 50%;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .custom-checkbox .custom-control-label::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-interestGroupsRow .ml-form-interestGroupsRowCheckbox .label-description::after, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description::after {
          background: no-repeat 50%/50% 50%;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-control, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-control {
        position: relative;
        display: block;
        min-height: 1.5rem;
        padding-left: 1.5rem;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-input, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-radio .custom-control-input, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-input, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-input {
          position: absolute;
          z-index: -1;
          opacity: 0;
          box-sizing: border-box;
          padding: 0;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-radio .custom-control-label, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-radio .custom-control-label, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-checkbox .custom-control-label, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-checkbox .custom-control-label {
          color: #000000;
          font-size: 12px!important;
          font-family: 'Open Sans', Arial, Helvetica, sans-serif;
          line-height: 22px;
          margin-bottom: 0;
          position: relative;
          vertical-align: top;
          font-style: normal;
          font-weight: 700;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-fieldRow .custom-select, #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow .custom-select {
        background-color: #ffffff !important;
        color: #333333 !important;
        border-color: #cccccc;
        border-radius: 4px !important;
        border-style: solid !important;
        border-width: 1px !important;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px !important;
        line-height: 20px !important;
        margin-bottom: 0;
        margin-top: 0;
        padding: 10px 28px 10px 12px !important;
        width: 100% !important;
        box-sizing: border-box !important;
        max-width: 100% !important;
        height: auto;
        display: inline-block;
        vertical-align: middle;
        background: url('https://cdn.mailerlite.com/images/default/dropdown.svg') no-repeat right .75rem center/8px 10px;
        -webkit-appearance: none;
        -moz-appearance: none;
        appearance: none;
      }


      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow {
        height: auto;
        width: 100%;
        float: left;
      }
      .ml-form-formContent.horozintalForm .ml-form-horizontalRow .ml-input-horizontal { width: 70%; float: left; }
      .ml-form-formContent.horozintalForm .ml-form-horizontalRow .ml-button-horizontal { width: 30%; float: left; }
      .ml-form-formContent.horozintalForm .ml-form-horizontalRow .ml-button-horizontal.labelsOn { padding-top: 25px;  }
      .ml-form-formContent.horozintalForm .ml-form-horizontalRow .horizontal-fields { box-sizing: border-box; float: left; padding-right: 10px;  }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow input {
        background-color: #ffffff;
        color: #333333;
        border-color: #cccccc;
        border-radius: 4px;
        border-style: solid;
        border-width: 1px;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px;
        line-height: 20px;
        margin-bottom: 0;
        margin-top: 0;
        padding: 10px 10px;
        width: 100%;
        box-sizing: border-box;
        overflow-y: initial;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow button {
        background-color: #000000 !important;
        border-color: #000000;
        border-style: solid;
        border-width: 1px;
        border-radius: 4px;
        box-shadow: none;
        color: #ffffff !important;
        cursor: pointer;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 14px !important;
        font-weight: 700;
        line-height: 20px;
        margin: 0 !important;
        padding: 10px !important;
        width: 100%;
        height: auto;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-horizontalRow button:hover {
        background-color: #333333 !important;
        border-color: #333333 !important;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow input[type="checkbox"] {
        box-sizing: border-box;
        padding: 0;
        position: absolute;
        z-index: -1;
        opacity: 0;
        margin-top: 5px;
        margin-left: -1.5rem;
        overflow: visible;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow .label-description {
        color: #000000;
        display: block;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 12px;
        text-align: left;
        margin-bottom: 0;
        position: relative;
        vertical-align: top;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow label {
        font-weight: normal;
        margin: 0;
        padding: 0;
        position: relative;
        display: block;
        min-height: 24px;
        padding-left: 24px;

      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow label a {
        color: #000000;
        text-decoration: underline;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow label p {
        color: #000000 !important;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif !important;
        font-size: 12px !important;
        font-weight: normal !important;
        line-height: 18px !important;
        padding: 0 !important;
        margin: 0 5px 0 0 !important;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow label p:last-child {
        margin: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedSubmit {
        margin: 0 0 20px 0;
        float: left;
        width: 100%;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedSubmit button {
        background-color: #000000 !important;
        border: none !important;
        border-radius: 4px !important;
        box-shadow: none !important;
        color: #ffffff !important;
        cursor: pointer;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif !important;
        font-size: 14px !important;
        font-weight: 700 !important;
        line-height: 21px !important;
        height: auto;
        padding: 10px !important;
        width: 100% !important;
        box-sizing: border-box !important;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedSubmit button.loading {
        display: none;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedSubmit button:hover {
        background-color: #333333 !important;
      }
      .ml-subscribe-close {
        width: 30px;
        height: 30px;
        background: url('https://cdn.mailerlite.com/images/default/modal_close.png') no-repeat;
        background-size: 30px;
        cursor: pointer;
        margin-top: -10px;
        margin-right: -10px;
        position: absolute;
        top: 0;
        right: 0;
      }
      .ml-error input, .ml-error textarea, .ml-error select {
        border-color: red!important;
      }

      .ml-error .custom-checkbox-radio-list {
        border: 1px solid red !important;
        border-radius: 4px;
        padding: 10px;
      }

      .ml-error .label-description,
      .ml-error .label-description p,
      .ml-error .label-description p a,
      .ml-error label:first-child {
        color: #ff0000 !important;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow.ml-error .label-description p,
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-checkboxRow.ml-error .label-description p:first-letter {
        color: #ff0000 !important;
      }
            @media only screen and (max-width: 400px){

        .ml-form-embedWrapper.embedDefault, .ml-form-embedWrapper.embedPopup { width: 100%!important; }
        .ml-form-formContent.horozintalForm { float: left!important; }
        .ml-form-formContent.horozintalForm .ml-form-horizontalRow { height: auto!important; width: 100%!important; float: left!important; }
        .ml-form-formContent.horozintalForm .ml-form-horizontalRow .ml-input-horizontal { width: 100%!important; }
        .ml-form-formContent.horozintalForm .ml-form-horizontalRow .ml-input-horizontal > div { padding-right: 0px!important; padding-bottom: 10px; }
        .ml-form-formContent.horozintalForm .ml-button-horizontal { width: 100%!important; }
        .ml-form-formContent.horozintalForm .ml-button-horizontal.labelsOn { padding-top: 0px!important; }

      }
    </style>

    <style type="text/css">

      .ml-mobileButton-horizontal { display: none; }

      #mlb2-2052560 .ml-mobileButton-horizontal button {

        background-color: #000000 !important;
        border-color: #000000 !important;
        border-style: solid !important;
        border-width: 1px !important;
        border-radius: 4px !important;
        box-shadow: none !important;
        color: #ffffff !important;
        cursor: pointer;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif !important;
        font-size: 14px !important;
        font-weight: 700 !important;
        line-height: 20px !important;
        padding: 10px !important;
        width: 100% !important;

      }

      @media only screen and (max-width: 400px) {
        #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-formContent.horozintalForm {
          padding: 0 0 10px 0 !important;
        }
        .ml-hide-horizontal { display: none !important; }
        .ml-form-formContent.horozintalForm .ml-button-horizontal { display: none!important; }
        .ml-mobileButton-horizontal { display: inline-block !important; margin-bottom: 20px;width:100%; }
        .ml-form-formContent.horozintalForm .ml-form-horizontalRow .ml-input-horizontal > div { padding-bottom: 0px !important; }
      }

    </style>
  <style type="text/css">
    @media only screen and (max-width: 400px) {
       .ml-form-formContent.horozintalForm .ml-form-horizontalRow .horizontal-fields {
        margin-bottom: 10px !important;
        width: 100% !important;
      }
    }
  </style>
    
    <style type="text/css">
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions { text-align: left; float: left; width: 100%; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent {
        margin: 0 0 15px 0;
        text-align: left;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent.horizontal {
        margin: 0 0 15px 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent h4 {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 12px;
        font-weight: 700;
        line-height: 18px;
        margin: 0 0 10px 0;
        word-break: break-word;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent p {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 12px;
        line-height: 18px;
        margin: 0 0 10px 0;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent.privacy-policy p {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 12px;
        line-height: 22px;
        margin: 0 0 10px 0;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent.privacy-policy p a {
        color: #000000;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent.privacy-policy p:last-child {
        margin: 0;
      }

      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent p a {
        color: #000000;
        text-decoration: underline;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent p:last-child { margin: 0 0 15px 0; }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptions {
        margin: 0;
        padding: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox {
        margin: 0 0 10px 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox:last-child {
        margin: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox label {
        font-weight: normal;
        margin: 0;
        padding: 0;
        position: relative;
        display: block;
        min-height: 24px;
        padding-left: 24px;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .label-description {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 12px;
        line-height: 18px;
        text-align: left;
        margin-bottom: 0;
        position: relative;
        vertical-align: top;
        font-style: normal;
        font-weight: 700;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox .description {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 12px;
        font-style: italic;
        font-weight: 400;
        line-height: 18px;
        margin: 5px 0 0 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsOptionsCheckbox input[type="checkbox"] {
        box-sizing: border-box;
        padding: 0;
        position: absolute;
        z-index: -1;
        opacity: 0;
        margin-top: 5px;
        margin-left: -1.5rem;
        overflow: visible;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedMailerLite-GDPR {
        padding-bottom: 20px;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedMailerLite-GDPR p {
        color: #000000;
        font-family: 'Open Sans', Arial, Helvetica, sans-serif;
        font-size: 10px;
        line-height: 14px;
        margin: 0;
        padding: 0;
      }
      #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedMailerLite-GDPR p a {
        color: #000000;
        text-decoration: underline;

      }
      @media (max-width: 768px) {
        #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedPermissionsContent p {
          font-size: 12px !important;
          line-height: 18px !important;
        }
        #mlb2-2052560.ml-form-embedContainer .ml-form-embedWrapper .ml-form-embedBody .ml-form-embedPermissions .ml-form-embedMailerLite-GDPR p {
          font-size: 10px !important;
          line-height: 14px !important;
        }
      }
    </style>

    
    

    
    

    

      
        
        
      

      
        
        
      

      

            
            
            
            
            
            
      

      

      
        
        
         
        
        
      

        
        
        
        
        
        
      

       

        
        
        
        
        
        
        
       


      
        
        
        
        
  



  
        
        
        
      


      
    
    
    
    
    
  

  
        
        
        
        
        
      

      
        
        
        
        
        
      

      
        
        
        
        
        
      

       

        
        
        
        
       

       
        
        
        
        
      

      
        
        
        
        
        
        
        
       

    

    


      


      

      
      

      

      





    

      
    <div id="mlb2-2052560" class="ml-form-embedContainer ml-subscribe-form ml-subscribe-form-2052560">
      <div class="ml-form-align-center ">
        <div class="ml-form-embedWrapper embedForm">

          
          

          <div class="ml-form-embedBody ml-form-embedBodyHorizontal row-form">

            <div class="ml-form-embedContent" style=" ">
              
                <h4>TipTop Feedback news</h4>
                <p>Follow my journey on making teacher evaluation and feedback easier,&nbsp;simpler and faster.&nbsp;</p>
              
            </div>

            <form class="ml-block-form" action="https://assets.mailerlite.com/jsonp/213538/forms/71113837799016091/subscribe" data-code="" method="post" target="_blank">
              

              <div class="ml-form-formContent horozintalForm">
                <div class="ml-form-horizontalRow">
                  <div class="ml-input-horizontal">
                    
                      
                      <div style="width: 100%;" class="horizontal-fields">






                        <div class="ml-field-group ml-field-email ml-validate-email ml-validate-required">
                          
                          <!-- input -->
                      <input type="email" class="form-control" data-inputmask="" name="fields[email]" placeholder="Email" autocomplete="email">
                      <!-- /input -->
                        </div>



                      </div>
                    
                  </div>


                  <div class="ml-button-horizontal primary ">
                    
                      <button type="submit" class="primary">Subscribe</button>
                    
                    <button disabled="disabled" style="display: none;" type="button" class="loading">
                      <div class="ml-form-embedSubmitLoad"></div>
                      <span class="sr-only">Loading&#8230;</span>
                    </button>
                  </div>
                </div>
              </div>

              <!-- Privacy policy -->
              <div class="ml-form-embedPermissions" style="">
                <div class="ml-form-embedPermissionsContent horizontal privacy-policy">

                  

                  <p>You can unsubscribe anytime.</p>

                  
                </div>
              </div>
              <!-- /Privacy policy -->

              

              

              






              
              <input type="hidden" name="ml-submit" value="1">

              

              <div class="ml-mobileButton-horizontal">
                <button type="submit" class="primary">Subscribe</button>
                <button disabled="disabled" style="display: none;" type="button" class="loading">
                  <div class="ml-form-embedSubmitLoad"></div>
                  <span class="sr-only">Loading&#8230;</span>
                </button>
              </div>
              <input type="hidden" name="anticsrf" value="true">
            </form>
          </div>

          <div class="ml-form-successBody row-success" style="display: none">

            <div class="ml-form-successContent">
              
                <h4>Thank you!</h4>
                <p>You have successfully joined the TipTop Feedback&nbsp;subscriber list.</p>
              
            </div>

          </div>
        </div>
      </div>
    </div>

  

  
  
  <script>
    function ml_webform_success_2052560() {
      var $ = ml_jQuery || jQuery;
      $('.ml-subscribe-form-2052560 .row-success').show();
      $('.ml-subscribe-form-2052560 .row-form').hide();
    }
      </script>
  
  
      <script src="https://groot.mailerlite.com/js/w/webforms.min.js?v491724307ca3b85c1c754857e93994e5" type="text/javascript"></script>



<p></p>



<p></p>



<p></p>
<p>The post <a href="https://www.kasperkamperman.com/edu/student-evaluation-of-teaching/">Student evaluation of teaching</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.kasperkamperman.com/edu/student-evaluation-of-teaching/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Arduino &#8211; How to fix error .h: No Such File or Directory</title>
		<link>https://www.kasperkamperman.com/blog/arduino-how-to-fix-error-h-no-such-file-or-directory/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Thu, 07 Oct 2021 12:57:18 +0000</pubDate>
				<category><![CDATA[Arduino]]></category>
		<category><![CDATA[Codelog]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=3229</guid>

					<description><![CDATA[<p>I ran into this &#8220;No Such File or Directory&#8221; myself when I downloaded a sketch from the IoT Cloud environment. I opened it up in the Arduino IDE. When I pressed Sketch Upload I got this precise error message &#8220;ArduinoIoTCloud.h: No such file or directory&#8220;. If you run into these errors, the chance is big ... <a title="Arduino &#8211; How to fix error .h: No Such File or Directory" class="read-more" href="https://www.kasperkamperman.com/blog/arduino-how-to-fix-error-h-no-such-file-or-directory/" aria-label="More on Arduino &#8211; How to fix error .h: No Such File or Directory">Read more</a></p>
<p>The post <a href="https://www.kasperkamperman.com/blog/arduino-how-to-fix-error-h-no-such-file-or-directory/">Arduino &#8211; How to fix error .h: No Such File or Directory</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>I ran into this &#8220;No Such File or Directory&#8221; myself when I downloaded a sketch from the IoT Cloud environment. I opened it up in the Arduino IDE. When I pressed Sketch Upload I got this precise error message &#8220;<span class="has-inline-color has-vivid-red-color">ArduinoIoTCloud.h: No such file or directory</span>&#8220;.</p>



<p>If you run into these errors, the chance is big that you forgot to import a Library in your Arduino IDE.  </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="576" src="https://cdn.kasperkamperman.com/media//no-such-file-error-800x576.png" alt="" class="wp-image-3230" srcset="https://kasperkamperman.com/media/no-such-file-error-800x576.png 800w, https://kasperkamperman.com/media/no-such-file-error-480x346.png 480w, https://kasperkamperman.com/media/no-such-file-error-768x553.png 768w, https://kasperkamperman.com/media/no-such-file-error.png 1110w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>.h No such file or directory</figcaption></figure>



<p>Unfortunately, Arduino doesn&#8217;t give hints that can help beginners (<a href="https://processing.org">Processing</a>, a creative software tool, does a better job in this).</p>



<p>You get this &#8220;No such file or directory&#8221; error when you don&#8217;t install a library. The default functionality in Arduino can be extended by libraries. In this case, a library that includes all the functionality to connect to the Arduino IoT Cloud service. </p>



<h2 class="wp-block-heading">How do you figure out which library is missing when you get the &#8220;no such file or directory&#8221; error?</h2>



<p>My tip is to search for the plain file. So, in this situation, <strong>&#8220;ArduinoIoTCloud.h&#8221;</strong>. Most Arduino code is open-source and published on GitHub. Lucky for us, code shared on Github is well-indexed by engines like Google and DuckDuckGo. </p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="783" height="321" src="https://cdn.kasperkamperman.com/media//Screenshot-2021-10-07-at-14.31.52.png" alt="" class="wp-image-3231" srcset="https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.31.52.png 783w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.31.52-480x197.png 480w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.31.52-768x315.png 768w" sizes="(max-width: 783px) 100vw, 783px" /><figcaption>Searching for the missing .h file ArduinoIoTCloud.h</figcaption></figure>



<p>This will lead you to <a href="https://github.com/arduino-libraries/ArduinoIoTCloud/blob/master/src/ArduinoIoTCloud.h" rel="nofollow">ArduinoIoTCloud/src/ArduinoIoTCloud.h</a> </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="529" src="https://cdn.kasperkamperman.com/media//Screenshot-2021-10-07-at-14.34.08-800x529.png" alt="" class="wp-image-3232" srcset="https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.34.08-800x529.png 800w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.34.08-480x317.png 480w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.34.08-768x508.png 768w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.34.08.png 920w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>ArduinoIoTCloud.h on Github</figcaption></figure>



<h2 class="wp-block-heading">How do you install an Arduino library?</h2>



<p>You can download the code from Github and copy it to your library directory. </p>



<p>An easier way is to add the library in the Library Manager.</p>



<p>Go to Sketch &gt; Include Library &gt; Manage Libraries &#8230;<br>Type in the name &#8220;ArduinoIoTCloud&#8221;, select it in the list and press Install. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="452" src="https://cdn.kasperkamperman.com/media//Screenshot-2021-10-07-at-14.39.17-800x452.png" alt="" class="wp-image-3233" srcset="https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.39.17-800x452.png 800w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.39.17-480x271.png 480w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.39.17-400x225.png 400w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.39.17-768x434.png 768w, https://kasperkamperman.com/media/Screenshot-2021-10-07-at-14.39.17.png 807w" sizes="(max-width: 800px) 100vw, 800px" /></figure>



<p>Press Install and make sure you install the library, including all of its dependencies (other libraries that this library uses). </p>



<p>I hope I could help you move forward. Have fun playing with Arduino and check some of my other articles in my <a href="https://www.kasperkamperman.com/arduino/">Arduino category</a>. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/arduino-how-to-fix-error-h-no-such-file-or-directory/">Arduino &#8211; How to fix error .h: No Such File or Directory</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Programming in Processing for beginners</title>
		<link>https://www.kasperkamperman.com/blog/programming-in-processing-for-beginners/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Fri, 17 Sep 2021 12:58:38 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=3160</guid>

					<description><![CDATA[<p>Learn programming in Processing with this tutorial series for beginners. In this series I recreate the classic Simon Says game, step by step. It&#8217;s a great way to learn to create a small but complete digital product (in this case a game) from scratch. We start with reverse engineering the Simon game. In the introduction ... <a title="Programming in Processing for beginners" class="read-more" href="https://www.kasperkamperman.com/blog/programming-in-processing-for-beginners/" aria-label="More on Programming in Processing for beginners">Read more</a></p>
<p>The post <a href="https://www.kasperkamperman.com/blog/programming-in-processing-for-beginners/">Programming in Processing for beginners</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Learn programming in <a href="https://processing.org/">Processing</a> with this tutorial series for beginners. In this series I recreate the classic Simon Says game, step by step. It&#8217;s a great way to learn to create a small but complete digital product (in this case a game) from scratch.</p>



<p>We start with reverse engineering the Simon game. In the introduction video I show how we can reverse engineer the game (thanks to Simon Inn for his in-depth article on <a href="https://www.waitingforfriday.com/?p=586">reverse engineering the electronic Simon Game</a>) and break down a programming problem in different steps. Breaking apart a bigger problem in multiple smaller steps is one of the most important skills a developer should master. </p>



<p>This tutorial series is suited for beginning/intermediate programmers. In the list below you can read what you&#8217;ll learn in each video. The code for each programming in Processing video is available on <a href="https://github.com/SaxionACT-Art-and-Technology/Processing-Simon-Says">Github</a>. </p>



<h3 class="wp-block-heading"><a href="https://www.youtube.com/watch?v=VU_SQur0eHg&amp;list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd&amp;index=1">Introduction</a> (12 minutes)<br><em>Reverse Engineering the MB Simon Says game.</em></h3>



<ul><li>Inspecting the original game.</li><li>Breaking apart the functionality of the game in small development steps.</li></ul>



<h3 class="wp-block-heading"><a href="https://www.youtube.com/watch?v=VU_SQur0eHg&amp;list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd&amp;index=2">Illuminated buttons</a> (33 minutes)<br><em>Creating four buttons that light up on a mouse-click.</em></h3>



<ul><li>Drawing in Processing (fill, rect, width, height).</li><li>Using the <a href="https://processing.org/reference/">Processing Reference</a>.</li><li>Creating a button object (class, void).</li><li>Displaying four buttons (for-loop, [] array).</li><li>Illuminated buttons (lerpColor).</li><li>Handle mouse clicks (if, &amp;&amp; logical operator, else, mousePressed, mouseReleased, println).</li></ul>



<h3 class="wp-block-heading"><a href="https://www.youtube.com/watch?v=VU_SQur0eHg&amp;list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd&amp;index=3">Tone Generator</a> (25 minutes)<br><em>Generate tones with the Processing Sound library and trigger the notes with the buttons.</em></h3>



<ul><li>Using the <a href="https://processing.org/reference/libraries/sound/index.html">Sound library</a>.</li><li>Creating a Simon Tone Generator object.</li><li>Use the Square Wave Oscillator (SqrOsc, play, amp).</li><li>Generate a unique tone for each button (freq). </li><li>Program a timer that controls the note duration (millis).</li></ul>



<h3 class="wp-block-heading"><a href="https://www.youtube.com/watch?v=VU_SQur0eHg&amp;list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd&amp;index=4">Simon Speaks</a> (21 minutes)<br><em>Let Simon say a sentence of random words.</em></h3>



<ul><li>Light up the buttons based on the tone length. </li><li>Play random notes (random, int() typecasting).</li><li>Play random notes with a pause in between (millis).</li><li>Let Simon generate a sentence.</li><li>Let Simon play the sentence word by word (int, [] array).</li><li>Deal with an ArrayOutOfBoundsException error.</li><li>Join an array (sentence) and print it to the console (join(), printArray).</li></ul>



<h3 class="wp-block-heading"><a href="https://www.youtube.com/watch?v=VU_SQur0eHg&amp;list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd&amp;index=5">User talks, Simon checks</a> (12 minutes)<br><em>The user &#8220;talks&#8221; and Simon checks if the user said the correct sentence. If not Simon generates a new sentence.</em></h3>



<ul><li>Check if the button matches the current word in mousePressed (!= inequality relational operator).</li><li>Advance in the sentence when the button matches the word in mouseReleased.</li><li>Start over when the button doesn&#8217;t match the word in mouseReleased (boolean variable).</li><li>Debugging. Reset the position in the sentence to the start.</li></ul>



<h3 class="wp-block-heading"><a href="https://www.youtube.com/watch?v=TN70A4T-uAE&amp;list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd&amp;index=6">Wrap up everything in one final game</a> (30 minutes) <br><em>Create the turn based game (Simon, User, Simon, User, etc.) and the difficulty curve.</em></h3>



<ul><li>Implement the difficulty curve. </li><li>Each time you repeated the correct word, Simon makes the sentence one word longer. </li><li>Give feedback to the users if they won or not. </li><li>Debugging sentence length (println) and find I forgot to reset the sentence length.</li><li>Make Simon talk faster with longer sentences.</li><li>Create a dialog and show it on screen (text, textSize, textAlign). </li><li>Game states introduction and ideas.</li></ul>



<p>Check out the whole programming Simon Says in Processing&nbsp;playlist on <a href="https://www.youtube.com/playlist?list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd">YouTube</a>. The whole series, with 6 videos, will take you about 135 minutes to watch.</p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button is-style-outline"><a class="wp-block-button__link has-black-color has-cyan-bluish-gray-background-color has-text-color has-background" href="https://github.com/SaxionACT-Art-and-Technology/Processing-Simon-Says">Get the code on GitHub </a></div>



<div class="wp-block-button is-style-outline"><a class="wp-block-button__link has-white-color has-luminous-vivid-orange-to-vivid-red-gradient-background has-text-color has-background" href="https://www.youtube.com/playlist?list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd">Check the tutorial playlist on YouTube</a></div>
</div>


<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube"><a href="https://www.kasperkamperman.com/blog/programming-in-processing-for-beginners/"><img decoding="async" src="https://www.kasperkamperman.com/wordpress_kk/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=https%3A%2F%2Fi.ytimg.com%2Fvi%2FVU_SQur0eHg%2Fhqdefault.jpg" alt="YouTube Video"></a><br />Watch this playlist <a href="https://www.youtube.com/playlist?list=PLDnfaWhAwvwdrydaRuG8J8v8LQmrzIyXd">on YouTube</a><br /><figcaption></figcaption></figure><p>The post <a href="https://www.kasperkamperman.com/blog/programming-in-processing-for-beginners/">Programming in Processing for beginners</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Technical Prototyping a Compass Web App</title>
		<link>https://www.kasperkamperman.com/blog/technical-prototyping-a-compass-web-app/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Fri, 10 Sep 2021 23:36:14 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<category><![CDATA[compass]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[svg]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=3137</guid>

					<description><![CDATA[<p>Idea A while ago, I visited a look-out spot on the Holterberg. &#8220;Berg&#8221; means mountain in Dutch. In a flat country like The Netherlands we even call our smallest hills proudly mountains. Nonetheless, while I was there, I wondered which cities I could actually see in the distance. Of course, I could open up Google ... <a title="Technical Prototyping a Compass Web App" class="read-more" href="https://www.kasperkamperman.com/blog/technical-prototyping-a-compass-web-app/" aria-label="More on Technical Prototyping a Compass Web App">Read more</a></p>
<p>The post <a href="https://www.kasperkamperman.com/blog/technical-prototyping-a-compass-web-app/">Technical Prototyping a Compass Web App</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Idea</h2>



<p>A while ago, I visited a look-out spot on the Holterberg. &#8220;Berg&#8221; means mountain in Dutch. In a flat country like The Netherlands we even call our smallest hills proudly mountains. Nonetheless, while I was there, I wondered which cities I could actually see in the distance. Of course, I could open up Google Maps, but I thought it was nice to create a more straightforward solution. The idea came up to create a compass showing which cities are located in different directions and their distance. I developed a small prototype in two days. My goal was to play with some web technologies (Sensor API&#8217;s, SVG rendering)  and create a technical prototype. <br><div class="su-youtube su-u-responsive-media-yes"><iframe loading="lazy" width="600" height="400" src="https://www.youtube.com/embed/UZx-dmwhFS8?" frameborder="0" allowfullscreen allow="autoplay; encrypted-media; picture-in-picture" title=""></iframe></div>



<h2 class="wp-block-heading">Technical prototype concept</h2>



<p>The phone&#8217;s compass sensor returns a value between 0-360 degrees, depending on how your phone is positioned relative to the magnetic north. Based on the positioning of the phone, the &#8220;compass base&#8221; (the view on the screen) shows the cities name in the viewport. </p>



<p>The &#8220;compass base&#8221; is an SVG image. For each user location (latitude, longitude), the &#8220;compass base&#8221; is unique. Based on the location of the user and the city locations, we can calculate which city should be visible on each compass bearing. We position the city name on the SVG based on that initial bearing.   <br><br>Based on the phone&#8217;s direction, we get the angle. The higher that angle value, the more we translate the canvas to the left. Giving the user the experience that the canvas scrolls to the right.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="266" src="https://cdn.kasperkamperman.com/media//compass_svg_concept1-1-800x266.png" alt="" class="wp-image-3146" srcset="https://kasperkamperman.com/media/compass_svg_concept1-1-800x266.png 800w, https://kasperkamperman.com/media/compass_svg_concept1-1-480x159.png 480w, https://kasperkamperman.com/media/compass_svg_concept1-1-768x255.png 768w, https://kasperkamperman.com/media/compass_svg_concept1-1-1536x510.png 1536w, https://kasperkamperman.com/media/compass_svg_concept1-1-2048x680.png 2048w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>Visualisation of the concept</figcaption></figure>



<h2 class="wp-block-heading">SVG Canvas concept</h2>



<p>The first thing I developed for this technical prototype was the SVG canvas concept. Moving your phone in different directions should be a seamless and endless experience. Moving from 360° to 0° and onwards and the other way round should be possible without a hick-up. You see the &#8220;edge-cases&#8221; in the picture below, where the phone is around 0 and 360 degrees. As you can see, we need to add a copy of the canvas at both sides of the &#8220;main&#8221; canvas. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="104" src="https://cdn.kasperkamperman.com/media//compass_svg_concept2-800x104.png" alt="" class="wp-image-3148" srcset="https://kasperkamperman.com/media/compass_svg_concept2-800x104.png 800w, https://kasperkamperman.com/media/compass_svg_concept2-480x63.png 480w, https://kasperkamperman.com/media/compass_svg_concept2-768x100.png 768w, https://kasperkamperman.com/media/compass_svg_concept2-1536x200.png 1536w, https://kasperkamperman.com/media/compass_svg_concept2-2048x267.png 2048w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>SVG Canvas used three times.</figcaption></figure>



<p>In SVG you can define an object by using <code>&lt;def&gt;&lt;/def&gt;</code> tags. With the &lt;use&gt; tag you can later re-use this object, multiple times throughout your SVG. So we only add elements once to our canvas, and they are display three times at different positions.</p>



<p>The group (g) with the compass_base id, is draw multiple times.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;htmlmixed&quot;,&quot;mime&quot;:&quot;text/html&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;HTML&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;html&quot;}">
&lt;svg id=&quot;compass&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot; xmlns:xlink=&quot;http://www.w3.org/1999/xlink&quot;
viewBox=&quot;0 0 10800 600&quot; width=&quot;10800&quot;&gt;
  &lt;defs&gt;
    &lt;g id=&quot;compass_base&quot;&gt;
      &lt;g id=&quot;compass_degrees&quot;&gt;&lt;/g&gt;
      &lt;g id=&quot;compass_cardinaldirs&quot;&gt;&lt;/g&gt;
      &lt;g id=&quot;compass_cities&quot;&gt;&lt;/g&gt;
    &lt;/g&gt;
  &lt;/defs&gt;  
  &lt;use xlink:href=&quot;#compass_base&quot; x=&quot;0&quot; y=&quot;0&quot; /&gt;
  &lt;use xlink:href=&quot;#compass_base&quot; x=&quot;3600&quot; y=&quot;0&quot; /&gt;
  &lt;use xlink:href=&quot;#compass_base&quot; x=&quot;7200&quot; y=&quot;0&quot; /&gt;
&lt;/svg&gt;
</pre></div>


<h3 class="wp-block-heading">Smooth motion</h3>



<p>If the compass is at <meta charset="utf-8">0°, the SVG is positioned at an x of -3600px plus half of the innerWidth of the window. When the angle goes up, we translate the SVG gradually more to the left (until -7200 plus half innerWidth) so it appears that the canvas scrolls to the right. </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function setCompassCenterPosition() {
    compassCenterPosition = -3600 + (0.5 * window.innerWidth);
}
</pre></div>


<p>At the moment we cross from 359° to 0°, we translate the SVG translates back to the 0 position (-3600 plus half innerWidth). This looks seamless to the user, because we have a canvas at both sides. </p>



<p>The compass output was bit jumpy, we could smooth that with an algorithm in JavaScript (<a href="https://www.kasperkamperman.com/blog/arduino/arduino-programming-map-and-smooth/">read this article on how I smooth sensor values on an Arduino</a>). We can use requestAnimationFrame to calculate the smoothed translate animation, but it&#8217;s better to let the browser handle this. With <a href="https://www.w3schools.com/css/css3_transitions.asp">CSS3 transitions</a> you can create smooth animations. </p>



<p>Using transitions works fine, until you cross the 0° degrees point. At that point we do translate the SVG from -7200px (+ innerWidth) to -3600px (+ innerWidth). Of course the user shouldn&#8217;t notice this big translation, so we can&#8217;t have the transition at that point. <br><br>I solve this by temporary setting the transition duration to 0 and then back to the original setting (by setting a wasDisableSmoothTransition flag). You still notice the zero degrees cross mark a bit, but it doesn&#8217;t disturb the UX. For a technical prototype is sophisticated enough. </p>



<p>Below the moveCompass function, which takes care of the translation of the SVG element. It accepts a value between 0-360 degrees.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function moveCompass(angle) {
    
    let position = compassCenterPosition - (angle * 10);
    let angleDelta = lastAngle-angle;

    if(wasDisabledSmoothTransition) {
        document.getElementById(&quot;compass&quot;).style.transitionDuration = &quot;150ms&quot;;
    }

    if(Math.abs(angleDelta) &gt; 180) {
        // crossed 0-360 line
        // temporary switch of CSS transitions
        document.getElementById(&quot;compass&quot;).style.transitionDuration = &quot;0ms&quot;;
        wasDisabledSmoothTransition = true;
    }

    lastAngle = angle;

    document.getElementById(&quot;compass&quot;).style.transform = &#039;translateX(&#039;+position+&#039;px)&#039;;
}
</pre></div>


<h3 class="wp-block-heading">Adding elements to the SVG Canvas</h3>



<p>For this technical prototype, I only added text to the SVG, but you could add anything. In the HTML code (first code block in this article), you can see I made different groups (&lt;g&gt;) with the IDs: compass_degrees, compass_cardinaldirs, compass_cities. <br><br>With the addText function I can add &lt;text&gt; elements to those specific groups. Since the width of the SVG is 3600px, I multiply the degrees *10 to calculate the x position.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function addText(text, degrees, distance, element) { 
    let x = degrees*10;
    let y = distance;
    let textEl = document.createElementNS(svgNS,&quot;text&quot;);
    textEl.setAttribute(&quot;x&quot;,x);
    textEl.setAttribute(&quot;y&quot;,y);
    textEl.setAttributeNS(null,&quot;alignment-baseline&quot;,&quot;hanging&quot;);
    textEl.setAttributeNS(null,&quot;text-anchor&quot;,&quot;middle&quot;);
    let textNode = document.createTextNode(text);
    textEl.appendChild(textNode);

    element.appendChild(textEl);

    return textEl;
}
</pre></div>


<p>Elements will be added to the SVG in the render function. </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function renderSVG(data) {

  // display compass angles
  const elCompassDegrees = document.getElementById(&quot;compass_degrees&quot;);
    
  for (i = 0; i &lt; 24; ++i) {
    let degree = i*15;
    addText(degree,degree,0,elCompassDegrees);
  }

  //... to be continued below
</pre></div>


<h3 class="wp-block-heading">Prototyping element collision detection</h3>



<p>One challenge was left. There are multiple cities that lie on the same angle seen from your location. Although, I didn&#8217;t work on a specific algorithm to reflect distance, I figured that I needed to prevent overlapping text. The idea is that if there is already an element on that position, the next element (with the same or a nearby compass bearing) will be moved downwards. <br><br>Luckily there is a function, <a href="https://developer.mozilla.org/en-US/docs/Web/API/SVGGraphicsElement/getBBox">getBBox()</a>, that gives us back the bounding-box of an SVG element. We use this to check if elements overlap. If an newly added element overlaps with previous element, we move this element down (higher y-position). </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="800" height="146" src="https://cdn.kasperkamperman.com/media//compass_svg_concept3-800x146.png" alt="" class="wp-image-3152" srcset="https://kasperkamperman.com/media/compass_svg_concept3-800x146.png 800w, https://kasperkamperman.com/media/compass_svg_concept3-480x88.png 480w, https://kasperkamperman.com/media/compass_svg_concept3-768x140.png 768w, https://kasperkamperman.com/media/compass_svg_concept3-1536x281.png 1536w, https://kasperkamperman.com/media/compass_svg_concept3-2048x374.png 2048w" sizes="(max-width: 800px) 100vw, 800px" /><figcaption>Bounding Box collision detection</figcaption></figure>



<p>The hasCollision function checks if an element (the last added one), collides with all the other elements in that specific group (#compass_cities) in the SVG. </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function hasCollision(element) {

  const boxA = element.getBBox();

  // get all the children (previously added elements)
  const children = document.getElementById(&quot;compass_cities&quot;).getElementsByTagName(&#039;*&#039;);

  // we walk through children array in reverse
  // if we have one collision we jump out and return true
  // to prevent collision with itself (last added element) we do length - 2
  for (i = children.length-2; i &gt;= 0 ; i--) {
    let boxB = children[i].getBBox();

    if(intersectBox(boxA,boxB)) {
      return true;
    }
  }

  return false;
}

function intersectBox(a, b) {
    return (Math.abs((a.x + a.width/2) - (b.x + b.width/2)) * 2 &lt; (a.width + b.width)) &amp;&amp;
           (Math.abs((a.y + a.height/2) - (b.y + b.height/2)) * 2 &lt; (a.height + b.height));
}
</pre></div>


<p>In order to add the cities to the SVG canvas, we walk through our data (a json file with cities and their latitude longitude) and add the elements. When adding an element, we check if it collides with other elements and move the element down (higher y-position) if that&#8217;s the case. </p>



<p>To prevent a hanging browser, I made sure that the while-loop is not executed more than 100 times (preventEndlessLoop counter). This happend to me on iOS/MacOS while developing, I figured that getBBox on Safari returned the wrong values on a &lt;tspan&gt; element (that&#8217;s why I switched to &lt;text&gt; elements). So to be better safe than sorry I added this protection.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
// continued renderSVG function

// walk through the data
// calculate bearing and distance based on currentLocation and city latitude/longitude
// store angle and distance in the data array.
data.forEach((item) =&gt; {
  let angle_dist = getBearingDistancePrepare(currentLocation, item.ll);
  item.a = angle_dist[0].toPrecision(6);
  item.d = Math.round(angle_dist[1]);
});

// sort data on angle
data.sort((a, b) =&gt; (a.a &gt; b.a) ? 1 : -1);

const elCities = document.getElementById(&quot;compass_cities&quot;);

// walk through the data and add the citie names.
data.forEach((item) =&gt; {

  let startY = 60;
  const padding = 40;

  // Add the text to our compass_cities group. 
  let addedChildEl = addText(item.city,item.a, startY, elCities);

  let preventEndlessLoop = 0;

  // Check if this element collides. If so, change the y-position.
  while(preventEndlessLoop&lt;100 &amp;&amp; hasCollision(addedChildEl)) {
    startY = startY + padding;
    addedChildEl.setAttribute(&#039;y&#039;,startY);
    preventEndlessLoop++;
  }

});
</pre></div>


<h2 class="wp-block-heading">Calculating the distance and bearing</h2>



<p>In other to calculate the &#8220;as the crow flies&#8221; distance I used the algorithms shared by <a href="https://www.movable-type.co.uk/scripts/latlong.html">Chriss Veness (Movable Type)</a>. I combined two algorithms (distance and initial bearing), so slower cos/sin calculations can be re-used. </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function toDegrees (radians) {
    return radians* (180 / Math.PI);
}

function toRadians (degrees) {
    return degrees * (Math.PI / 180);
}

// https://www.movable-type.co.uk/scripts/latlong.html
// initial bearing formula
function getBearingDistance(lonlatA, lonlatB) {
    
    const R = 6371e3; // earth radius in meters
    const latA = toRadians(lonlatA[1]);
    const latB = toRadians(lonlatB[1]);
    const lonDelta = toRadians(lonlatB[0] - lonlatA[0]);

    // prevent double sin/cos calculations
    const sinlatA = Math.sin(latA);
    const coslatA = Math.cos(latA);
    const sinlatB = Math.sin(latB);    
    const coslatB = Math.cos(latB);
    const coslonDelta = Math.cos(lonDelta);

    // distance
    const distance = Math.acos(sinlatA * sinlatB + coslatA * coslatB  * coslonDelta) * R;

    // initial bearing
    const y = Math.sin(lonDelta) * coslatB ;
    const x = coslatA * sinlatB - sinlatA * coslatB * coslonDelta;
    let bearing = toDegrees(Math.atan2(y, x));

    if(bearing&lt;0) bearing += 360;
    
    return [bearing, distance/1000];
}
</pre></div>


<h2 class="wp-block-heading">Reader the compass with web technologies</h2>



<p>One of the biggest challenges provided to get a reliable compass reading on Android. Apple is not known for supporting API&#8217;s for the Web, but their compass implementation is simple and gives a reliable result. Getting the compass bearing is simple a question of calling <code>compassDir = e.webkitCompassHeading;</code> The only thing, is that you need to get user permission. Below the code, the startCompass function should be triggered by a user action (onClick). </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
function startCompass() {
	if (typeof DeviceOrientationEvent !== &#039;undefined&#039; &amp;&amp; 
    typeof DeviceOrientationEvent.requestPermission === &#039;function&#039;) {
      DeviceOrientationEvent.requestPermission()
        .then((response) =&gt; {
          if (response === &quot;granted&quot;) {
            window.addEventListener(&quot;deviceorientation&quot;, handler, true);
          } else {
            alert(&quot;has to be allowed!&quot;);
          }
        })
        .catch((error) =&gt; { 
            console.error(error);
            alert(&quot;not supported&quot;)
        });
}
  
function handler(e) {
   if (e.webkitCompassHeading) {
        compassDir = e.webkitCompassHeading;   
   }
}
</pre></div>


<p>I expected that reading a compass would be a trivial and easy task on Chrome Android too. Most StackOverflow answers mention that for Android just the alpha can be read (returned by the <code>deviceorientationabsolute</code> event). However, I didn&#8217;t seem to get reliable results on my Android device with those values. The needle went everywhere (even after calibration movements). I couldn&#8217;t let the web compass behave like native compass apps on Android. </p>



<p>I spend some time checking demos and came across this awesome <a href="https://richtr.github.io/Marine-Compass/index.html">Marine compass demo</a> by Rich Tibbett. This one seemed to give results that seemed to make sense (less then iOS) and the compass heading angle was similar to native apps (you have to refresh after loading, then it seems to work). <br><br>It uses his own <a href="https://github.com/MrBlenny/Full-Tilt">Full-Tilt API library</a> (most recent fork), which normalises the data. If you are interested, you should dive into Quarternions, Euler angles and more (I vaguely remember those terms from some Unity work I did). The library is not updated recently, but it seems to work ok. </p>



<p><a href="https://chrishewett.com/blog/device-orientation-test-page/">Chriss Hewett made a demo</a> which displays all the different API outputs. I also could dive into the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbsoluteOrientationSensor">AbsoluteOrientationSensor API</a>, to see if that gives some better compass readings. </p>



<h2 class="wp-block-heading">Continuation of this project</h2>



<p>The prototype is not live and released. For several reasons, both in UX as technically, I&#8217;m not sure if I will continue with this project in this form. <br><br>First, I figured out that when testing it myself, I got a bit motion sick. I handed it over to some other people, and they had the same experience. Probably moving around and looking at a screen is not the most comfortable user experience. Maybe I have to come up with a different design approach to solve this. </p>



<p>Secondly, placing 240 world cities on the compass was a nice idea, but I think it&#8217;s pretty overwhelming in UX terms. Maybe I should stick with the Use Case I started with (cities around a point close by). Rendering should also be optimised. Currently rendering the SVG (with 240 cities) costs about 50-60 seconds on a low-end Android phone. This could be solved by moving calculations to another thread (<a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers">WebWorkers</a>) or optimising the bearing/distance calculations. I could also create a backend and do calculations server-side.</p>



<p>Thirdly, to use geo location and access device orientation (compass) the user needs to give permission. For a good user experience, some effort is required to create screens that explain why location and sensor data is used.</p>



<p>LastIy the biggest issue was the difficulty of getting reliable compass readings. All of the above doesn&#8217;t really makes sense, if I can&#8217;t get a correct compass bearing.</p>



<h2 class="wp-block-heading">Final thoughts on the technical prototype</h2>



<p>I thought the compass idea might be interesting to move around in new cities. In many (touristic) cities then have those way-finding signs that point to touristic attractions, maybe this idea could be an addition to that. </p>



<p>Another iteration could be for education. Let pupils select/enter a city name and guess the direction. <br><br>Feel free to contact me if you have any questions or if you are looking for solutions related to this article. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/technical-prototyping-a-compass-web-app/">Technical Prototyping a Compass Web App</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Import CSV in MongoDB and keep it in sync.</title>
		<link>https://www.kasperkamperman.com/blog/web/import-csv-in-mongodb/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Tue, 20 Apr 2021 11:43:43 +0000</pubDate>
				<category><![CDATA[web]]></category>
		<category><![CDATA[csv]]></category>
		<category><![CDATA[mongodb]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=3037</guid>

					<description><![CDATA[<p>Goal I&#8217;m working on a store and business locator service. Instead of building a User Interface to add addresses to a database, I like to keep track of all the data in an Excel/Google Sheets spreadsheet. This article explains how I build a script to import CSV in MongoDB and keep it in sync with ... <a title="Import CSV in MongoDB and keep it in sync." class="read-more" href="https://www.kasperkamperman.com/blog/web/import-csv-in-mongodb/" aria-label="More on Import CSV in MongoDB and keep it in sync.">Read more</a></p>
<p>The post <a href="https://www.kasperkamperman.com/blog/web/import-csv-in-mongodb/">Import CSV in MongoDB and keep it in sync.</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Goal</h2>



<p>I&#8217;m working on a store and business locator service. Instead of building a User Interface to add addresses to a database, I like to keep track of all the data in an Excel/Google Sheets spreadsheet. This article explains how I build a script to import CSV in MongoDB and keep it in sync with the same CSV file.</p>



<p>I use PHP because I&#8217;m most familiar with it. It&#8217;s a bit of a challenge with MongoDB because PHP is not that JSON-friendly. In PHP, you have to rewrite the JSON operations in object/array notation.</p>



<p>I picked PHP because I have the most experience with it, and it runs on any cheap shared web hosting service. The downside of PHP is that it&#8217;s not really JSON friendly (query notation for MongoDB), so you need to rewrite the MongoDB queries into object/array notation for PHP. I use the MongoDB PHP driver and a locally installed database (calling a remote Atlas instance felt too slow when developing). However, you can use the script with the <a href="https://www.mongodb.com/cloud/atlas" target="_blank" rel="noreferrer noopener">free MongoDB Atlas instance</a> too. This will save you the local installation step. </p>



<p>Resources to start:</p>



<ul><li><a href="https://docs.mongodb.com/manual/installation/">Installing MongoDB Community edition locally</a></li><li><a href="https://docs.mongodb.com/drivers/php/" target="_blank" rel="noreferrer noopener">Instructions on installing and using the MongoDB PHP driver. </a></li><li><a href="https://www.mongodb.com/products/compass" target="_blank" rel="noreferrer noopener">Compass GUI</a></li></ul>



<h3 class="wp-block-heading">Why MongoDB?</h3>



<p>I chose MongoDB as database for several reasons:</p>



<ul><li>Geo functionalities out-of-the-box.</li><li>Free tools with a pleasant UI, like MongoDB Compass.</li><li>Good documentation, tutorials, and a proven track record.</li><li>Easy (and free) to set up cloud service: <a href="https://www.mongodb.com/cloud/atlas" target="_blank" rel="noreferrer noopener">MongoDB Atlas</a>.</li><li>A document database seems a good fit for the project I&#8217;m working on.</li></ul>



<p>The most common and universal spreadsheet export format is CSV (Comma Separated Values). Actually, there are plenty of ways to import a CSV in MongoDB. In MongoDB Compass, you create a new collection and drop a CSV file, and in the shell, you can run <code>mongoimport</code>.</p>



<h3 class="wp-block-heading">Requirements</h3>



<ol><li>I like the spreadsheet as a single source of truth (SSOT) for the address data. <ul><li>Remove a row in the CSV, should remove the document in MongoDB.</li><li>Remove a column in the CSV, should remove the key in each MongoDB document.</li><li>Empty a cell in the CSV should remove the key in MongoDB. </li></ul></li><li>I also like to clean up CSV data:<ul><li>Leave out empty fields (<code>ignoreBlanks</code> in <code><a href="https://docs.mongodb.com/database-tools/mongoimport/" target="_blank" rel="noreferrer noopener">mongoimport</a></code>).</li><li>Merge longitude and latitude fields into a single field to be used as a 2d sphere index (for fast geo lookups).</li></ul></li><li>Automate it later on with a daily cronjob or another trigger. </li></ol>



<p>Importing CSV data in MongoDB with <code><a href="https://docs.mongodb.com/database-tools/mongoimport/" target="_blank" rel="noreferrer noopener">mongoimport</a></code> works perfectly. However, deleting data from MongoDB when it&#8217;s removed from the CSV file needs more processing. So I decided to write my own code. This also allowed me to learn more about MongoDB. </p>



<h2 class="wp-block-heading">Steps</h2>



<ul><li>Read the CSV directly from Google sheets and loop through the rows.</li><li>Check if specific fields (address and city) exist (they serve as a unique index).</li><li>Check which fields are empty, remove them from the CSV object and unset them in MongoDB.</li><li>Prepare update/upsert query.</li><li>Run aggregation pipeline to check which rows are present in MongoDB and not in the CSV and prepare those for bulk delete.</li><li>BulkWrite the update/delete queries of those rows from MongoDB.</li></ul>



<h2 class="wp-block-heading">Learnings</h2>



<h3 class="wp-block-heading">Set default collation when creating a new collection.</h3>



<p>Adding addresses is a manual human operation, so mistakes (double spaces, forget acute symbols like é, capitals) can happen. We truly want to make sure addresses are unique and that the same address points to the same information. For example: &#8220;Dam 1 Amsterdam&#8221; is the same as &#8220;dam   1 amsterdam&#8221;. To make sure string comparison goes right, you can set collation in MongoDB:</p>



<blockquote class="wp-block-quote"><p>Collation allows users to specify language-specific rules for string comparison, such as rules for lettercase and accent marks. (<a href="https://docs.mongodb.com/master/reference/collation/" target="_blank" rel="noreferrer noopener">MongoDB collation documentation</a>)</p></blockquote>



<p>The best is to set a default collation; you don&#8217;t have to pass the collation parameters for each query. <strong>You can only do this during creation time. </strong></p>



<p>Pick any locale, most languages have the same <a href="https://docs.mongodb.com/manual/reference/collation-locales-defaults/#std-label-collation-languages-locales" target="_blank" rel="noreferrer noopener">default collation parameters</a> (like English(en) or Dutch (nl)).</p>



<p>Since I like to ignore case and other diacritics for lookup (like an acute accent, é), I picked <code>"strength: 1"</code> by default.  I also ignore whitespace and punctuation, &#8220;alternate&#8221; and &#8220;maxVariable<code>"</code>.</p>



<p>Mongo Compass has a built-in shell. Create a database with &#8220;use,&#8221; After that, the collection, named &#8220;mycollection.&#8221; </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
use mydatabase

db.createCollection(
	&quot;mycollection&quot;, 
	{ collation: 
		{ locale:&quot;en&quot;,strength:1, alternate:&quot;shifted&quot;, maxVariable:&quot;punct&quot;}
	}
)
</pre></div>


<p>You can also create a new database and collection in one step in MongoDB Compass. Use the settings above when you select &#8220;Custom Collation.&#8221;</p>



<p>Even with a collation set, I prefer to clean up the data as much as possible in PHP. I change all characters to lower-case and remove additional whitespaces to clean the data for displaying online later anyway (I will use <code>text-transform: capitalize;</code> CSS on address and city fields when displaying them).</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;php&quot;,&quot;mime&quot;:&quot;text/x-php&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;PHP&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;php&quot;}">
$row[&#039;address&#039;] = strtolower(preg_replace(&#039;/\s+/&#039;, &#039;&#039;, $row[&#039;address&#039;]));
$row[&#039;city&#039;]    = strtolower(preg_replace(&#039;/\s+/&#039;, &#039;&#039;, $row[&#039;city&#039;]));
</pre></div>


<h3 class="wp-block-heading">Unique compound index</h3>



<p>MongoDB has a default unique index field called _id. We don&#8217;t have that in our spreadsheet, which is the source of data. I needed to consider the fields unique that makes a row unique. In this address database case, that&#8217;s a combination of the address (street + house number) and city. There are cities with the same name, though, so we have to make sure that we add some indicators (like the province in the city name).</p>



<p>Luckily MongoDB supports unique compound indexes:</p>



<blockquote class="wp-block-quote"><p>If you use the unique constraint on a&nbsp;compound index, then MongoDB will enforce uniqueness on the&nbsp;combination&nbsp;of the index key values. (<a href="https://docs.mongodb.com/manual/core/index-unique/#unique-compound-index" target="_blank" rel="noreferrer noopener">MongoDb unique indexes documentation</a>)</p></blockquote>



<p>In the mongo shell, you can run the operation below. You can also create an index in the &#8220;indexes&#8221; tab in Compass.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;javascript&quot;,&quot;mime&quot;:&quot;text/javascript&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;JavaScript&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;js&quot;}">
db.mycollection.createIndex(
  { &quot;address&quot;: 1, &quot;city&quot;: 1 }, 
  { unique: true, name: &quot;direction&quot; } 
)
</pre></div>


<h3 class="wp-block-heading">Empty fields</h3>



<p>Updating or inserting based on CSV rows works perfect with:</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;php&quot;,&quot;mime&quot;:&quot;text/x-php&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;PHP&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;php&quot;}">
$foreach ($csv as $doc) { 
  filter = [ &#039;address&#039; =&gt; $doc[&#039;address&#039;], &#039;city&#039; =&gt; $doc[&#039;city&#039;] ];
  $mongo_bulk_write-&gt;update( 
    $filter,
    [&#039;$set&#039; =&gt; $doc], 
    [&#039;upsert&#039; =&gt; true] 
  );
}

$driver-&gt;executeBulkWrite(&#039;mydatabase.mycollection&#039;, $mongo_bulk_write);
</pre></div>


<p>This doesn&#8217;t deal with empty fields, though. I don&#8217;t include empty cells in the CSV file in the document to save data. So if the &#8220;address2&#8221; column or cell doesn&#8217;t have data, I won&#8217;t include the &#8220;address2&#8221; key-value pair (since the field is empty anyway).</p>



<p>A problem arises when removing a cell value from the CSV. I explicitly have to unset those fields; otherwise, old keys are not removed in MongoDB when updating.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;php&quot;,&quot;mime&quot;:&quot;text/x-php&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;PHP&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;php&quot;}">
// remove empty columns from the row
$unset = [];

foreach ($row as $key =&gt; $val) {
  if(empty($val)) {
    unset($row[$key]);
    // MongoDB needs an associative array with empty values for unset
    $unset[$key] = 1;
  }
}

$mongo_bulk_write-&gt;update(
 	$filter, 
	[ &#039;$set&#039; =&gt; $doc, &#039;$unset&#039; =&gt; $unset ], 
    [&#039;upsert&#039; =&gt; true]
);
</pre></div>


<h3 class="wp-block-heading">Remove a document in MongoDB when a row is removed from the CSV source.</h3>



<p>Upserting (<em>updating and inserting when an index is not found</em>) works with the above setup. The challenge is to remove documents from the MongoDB collection when rows are removed from the CSV file. <br><br>The idea is to look up documents that are not present as rows anymore in the CSV but still are present in the MongoDB collection. We also have to deal with address and city as a compound index. </p>



<p>This is solved by adding city and address together in one field. In the for-loop city and address values are merged, with # as a separator to split it later again and stored in an array.</p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;php&quot;,&quot;mime&quot;:&quot;text/x-php&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;PHP&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;php&quot;}">
// in the for-loop we merge city and address with # as seperator
$csv_compound[] = $doc[&#039;city&#039;].&#039;#&#039;.$doc[&#039;address&#039;];
</pre></div>


<p>To check this array in MongoDB, I use an <a href="https://docs.mongodb.com/manual/core/aggregation-pipeline/" target="_blank" rel="noreferrer noopener">aggregation pipeline</a>.<br>All credits go <a href="https://stackoverflow.com/questions/31663037/given-a-list-of-ids-whats-the-best-way-to-query-which-ids-do-not-exist-in-the" target="_blank" rel="noreferrer noopener">to this answer on StackOverflow</a>. </p>



<p>First, a list ($group) is made using the <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/concat/" target="_blank" rel="noreferrer noopener">$concat</a> function so that we can compare the <code>csv_compound</code> array with the resulting <code>mdb_compound</code> array. The <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/setDifference/" target="_blank" rel="noreferrer noopener">$setDifference</a> function is used to check the difference between the two arrays. The pipeline returns an array with the self-created compound indexes that are still in MongoDB but not in the CSV anymore. </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;php&quot;,&quot;mime&quot;:&quot;text/x-php&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;PHP&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;php&quot;}">
// in the for-loop we merge city and address with # as seperator
$group = [&#039;$group&#039; =&gt; [ &#039;_id&#039; =&gt; null, &#039;mdb_compound&#039; =&gt; [ &#039;$addToSet&#039; =&gt; [ &#039;$concat&#039; =&gt; [&#039;$city&#039;,&#039;#&#039;,&#039;$address&#039;] ] ] ]];
$project = [ &#039;$project&#039; =&gt; [ &#039;difference_mdb-sheets&#039; =&gt; [ &#039;$setDifference&#039; =&gt; [ &#039;$mdb_compound&#039;, $csv_compound] ], &#039;_id&#039; =&gt; 0 ] ];
$pipeline = array($group, $project);
$cursor = $collection-&gt;aggregate($pipeline);

$remove_from_mdb = json_decode(MongoDB\BSON\toJSON(MongoDB\BSON\fromPHP($cursor-&gt;toArray())), true)[0][&#039;difference_mdb-csv&#039;];
</pre></div>


<p>This created list is used to run a delete operation, with bulkwrite.  </p>


<div class="wp-block-codemirror-blocks-code-block code-block"><pre class="CodeMirror" data-setting="{&quot;showPanel&quot;:false,&quot;languageLabel&quot;:&quot;language&quot;,&quot;fullScreenButton&quot;:false,&quot;copyButton&quot;:true,&quot;mode&quot;:&quot;php&quot;,&quot;mime&quot;:&quot;text/x-php&quot;,&quot;theme&quot;:&quot;monokai&quot;,&quot;lineNumbers&quot;:true,&quot;styleActiveLine&quot;:false,&quot;lineWrapping&quot;:false,&quot;readOnly&quot;:true,&quot;fileName&quot;:&quot;&quot;,&quot;language&quot;:&quot;PHP&quot;,&quot;maxHeight&quot;:&quot;400px&quot;,&quot;modeName&quot;:&quot;php&quot;}">
foreach ($remove_from_mdb as $doc) {
	$doc = explode(&quot;#&quot;, $doc);
 	$filter = [ &#039;address&#039; =&gt; $doc[1], &#039;city&#039; =&gt; $doc[0] ];
    $mongo_bulk_write-&gt;delete($filter);
}
</pre></div>


<h3 class="wp-block-heading">Publish the CSV in Google Sheets to import it in MongoDB</h3>



<p>You don&#8217;t need an API to use <a href="https://docs.google.com/spreadsheets/" target="_blank" rel="noreferrer noopener">Google Sheets</a> as input. Just go to file > Publish to Web. Pick the CSV option and copy-paste the URL. By using that URL in PHP, we import the CSV in MongoDB. </p>



<p>I found that sometimes updates to the CSV (when saving) are a bit delayed. So you might have to wait a bit before running the PHP script. Of course, Sheets has plenty of opportunities for scripting, so you could even run the same progress I explained in PHP as an <a href="https://developers.google.com/apps-script/guides/sheets" target="_blank" rel="noreferrer noopener">Apps script</a>. However, I wanted to make a more general solution that I can utilize later with some web drag/drop interface</p>



<figure class="wp-block-image size-large is-resized is-style-default"><img loading="lazy" decoding="async" src="https://cdn.kasperkamperman.com/media//Screenshot-2021-04-20-at-19.06.21-800x607.png" alt="Export CSV from Google Sheets" class="wp-image-3094" width="615" height="467" srcset="https://kasperkamperman.com/media/Screenshot-2021-04-20-at-19.06.21-800x607.png 800w, https://kasperkamperman.com/media/Screenshot-2021-04-20-at-19.06.21-480x364.png 480w, https://kasperkamperman.com/media/Screenshot-2021-04-20-at-19.06.21-768x583.png 768w, https://kasperkamperman.com/media/Screenshot-2021-04-20-at-19.06.21.png 1228w" sizes="(max-width: 615px) 100vw, 615px" /></figure>



<h2 class="wp-block-heading">Show me the code </h2>



<p>You can find the complete script on Github including the Mongo dependency. </p>



<div class="wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex">
<div class="wp-block-button"><a class="wp-block-button__link" href="https://github.com/kasperkamperman/csv-to-mongodb/blob/main/csv_to_mongo.php" target="_blank" rel="noreferrer noopener">import csv in mongodb: csv_to_mongo.php</a></div>
</div>
<p>The post <a href="https://www.kasperkamperman.com/blog/web/import-csv-in-mongodb/">Import CSV in MongoDB and keep it in sync.</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Mobile First Camera Template (HTML, CSS, JS and WebRTC)</title>
		<link>https://www.kasperkamperman.com/blog/camera-template/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Sun, 17 May 2020 12:50:53 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<category><![CDATA[Computer Vision]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2767</guid>

					<description><![CDATA[<p>A mobile first camera template that serves as a starting point for developing a computer vision, AI vision application. Completely web-based (HTML,CSS,JS). It uses WebRTC and is intended for Android 7/8 (Chrome) and iOS 11 (and higher). </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/camera-template/">Mobile First Camera Template (HTML, CSS, JS and WebRTC)</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>I like to experiment with Computer Vision and AI API&#8217;s (like Azure Cognetive Services, Google Cloud Vision, IBM Watson) to see if I can utilise them for some ideas.</p>
<p>The most easy way to test those scripts and APIs them is by directly making a photo and sending image data to the API/script, instead of uploading files. I didn&#8217;t find a fast mobile first camera template for HTML5 as a starting point for my prototypes, so I developed one myself. The interface setup is mainly inspired by the standard Android and iOS Cameras.</p>
<p>The template doesn&#8217;t do anything with the image(canvas) data yet, I&#8217;ll leave that up to you.</p>
<p>Feel free to use it in your next Computer Vision or AI project.</p>
<p>Code was updated 17th of May 2020 (some fixes for iOS 13.4.1)</p>
<p><a class="fasc-button fasc-size-large fasc-type-popout" style="background-color: #000000; color: #ffffff;" href="https://demo.kasperkamperman.com/mobilecamtemplate/">Check out the Demo</a></p>
<p><a class="fasc-button fasc-size-large fasc-type-popout" style="background-color: #000000; color: #ffffff;" rel="nofollow" href="https://github.com/kasperkamperman/MobileCameraTemplate">Get the code on Github</a></p>
<h3>Requirements</h3>
<p>WebRTC is only supported on secure connections. So you need to serve it from https <em>(You can test and debug in Chrome from localhost although (this doesn&#8217;t work in Safari).</em></p>
<p>Because it utilises WebRTC you need a recent (mobile) OS and browser. It should work on Android, Firefox and iOS11 and Safari 11.</p>
<h3>Functionalities</h3>
<p>&#8211; Fullscreen mode<br />
&#8211; Take Photo<br />
&#8211; Flip Camera (environment / user)<br />
&#8211; Supports both portrait and landscape mode</p>
<h3>Used Libraries</h3>
<ul>
<li>Fullscreen functionality: <a href="https://github.com/sindresorhus/screenfull.js/">Screenfull.js</a></li>
<li><del>Detect WebRTC support: <a href="https://www.webrtc-experiment.com/DetectRTC/" rel="nofollow">DetectRTC.js</a></del></li>
<li>WebRTC cross-browser: <a href="https://github.com/webrtc/adapter">Adapter.js</a></li>
<li>UI click sound: <a href="https://howlerjs.com">Howler.js</a></li>
</ul>
<h3>Used Assets</h3>
<ul>
<li><a href="https://freesound.org/people/GameAudio/sounds/220200/">Basic Click Wooden sound</a>&nbsp;&#8211; <a href="https://www.gameaudio101.com">GameAudio</a></li>
<li><a href="https://material.io/icons/">Material Design Icons</a> (camera front, camera rear, photo camera, fullscreen, fullscreen exit)</li>
</ul>
<h3>Good WebRTC resources</h3>
<ul>
<li><a href="https://webrtc.github.io/samples/">webrtc.github.io/samples/</a></li>
<li><a href="https://www.webrtc-experiment.com/">webrtc-experiment.com/</a></li>
<li><a href="https://www.html5rocks.com/en/tutorials/getusermedia/intro/">html5rocks.com/en/tutorials/getusermedia/intro/</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia">developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia</a></li>
</ul>
<p>Credits and a link to this page are always appreciated.</p>
<p>I&#8217;m always curious how people end up using my stuff, so&nbsp;feel free to <a href="https://www.kasperkamperman.com/contact/">contact me</a>&nbsp;or <a href="https://twitter.com/kasperkamperman">send a tweet@kasperkamperman</a>.</p>
<h3>&nbsp;</h3>
<p>The post <a href="https://www.kasperkamperman.com/blog/camera-template/">Mobile First Camera Template (HTML, CSS, JS and WebRTC)</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Save Image to Web with Processing and PHP</title>
		<link>https://www.kasperkamperman.com/blog/processing-save-image-to-web/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Mon, 12 Feb 2018 08:54:50 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<category><![CDATA[Processing]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2659</guid>

					<description><![CDATA[<p>Processing demo code that shows how to upload an image from Processing to a web server. It takes a picture with your webcam and saves the image to a folder on your webserver with PHP. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/processing-save-image-to-web/">Save Image to Web with Processing and PHP</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>For certain interactive installations (like the <a href="https://www.kasperkamperman.com/projects/skyradio-photobooth/">Sky Radio Photo booth</a>) I need to be able to save and upload images to a webserver. There are different examples in the Processing forum, but not one clear and easy solution.&nbsp;</p>
<p>Actually the code is largely based on my <a href="https://www.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/">Processing Azure Face API code</a>, however instead of sending the image to Microsoft Azure, the image is uploaded to a web server and stored with PHP. Like the Azure example this code uses the&nbsp;Apache HTTP client library (from the <a href="https://hc.apache.org">Apache HTTPComponents project</a>). It&#8217;s already included in the sketch folder, so you don&#8217;t have to download or install it separately.&nbsp;</p>
<p><a class="fasc-button fasc-size-large fasc-type-flat ico-fa fasc-ico-before fa-github-alt" style="background-color: #000000; color: #ffffff;" href="https://github.com/kasperkamperman/Processing-SaveImageToWeb">Get the code on Github</a></p>
<h3>Web Server configuration</h3>
<p>First make sure you have a host that supports PHP.&nbsp;</p>
<p>Create a folder called &#8220;processing_upload&#8221; on the location you want (or modify the $targetDir variable in saveImage.php). Place saveImage.php in the same folder as you&#8217;ve placed the processing_upload folder (pay attention don&#8217;t place saveImage.php within processing_upload).</p>
<p>Don&#8217;t forget to set the file permissions of the folder to write (755).</p>
<h3>Modify the Processing sketch</h3>
<p>In the Processing sketch you have to point the </p>
<pre class="lang:default decode:1 inline:1 " >String baseUrl</pre>
<p>&nbsp; (in ImageUploader.pde) to the folder on your own server where you saved the saveImage.php file.&nbsp;</p>
<h3>Credits</h3>
<ul>
<li><a href="https://gist.github.com/jeffThompson/ea54b5ea40482ec896d1e6f9f266c731">JPEG compression sketch by Jeff Thompson.</a></li>
<li><a href="http://www.baeldung.com/httpclient-multipart-upload">Multipart uploader example by&nbsp;Eugen Paraschiv (Baeldung).&nbsp;</a></li>
<li><a href="https://www.w3schools.com/php/php_file_upload.asp">PHP saveImage file from W3Schools.</a></li>
<li><a href="https://hc.apache.org/downloads.cgi">Apache HTTPClient binary.</a></li>
</ul>
<p>&nbsp;</p>
<p>The post <a href="https://www.kasperkamperman.com/blog/processing-save-image-to-web/">Save Image to Web with Processing and PHP</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Using Azure Cognitive Services with Processing</title>
		<link>https://www.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Tue, 30 Jan 2018 10:03:51 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<category><![CDATA[Processing]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2622</guid>

					<description><![CDATA[<p>Processing demo code to sent images to the Microsoft Azure Cognitive Services Face API. It takes a picture from your webcam and it will return an analysis of all the faces found. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/">Using Azure Cognitive Services with Processing</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>For an upcoming exhibition I wanted to detect some face attributes like age, &nbsp;gender and emotion. This is something that&#8217;s not directly included in OpenCV (OpenCV library for Processing). To me it seemed most easy to use an API, so I looked into Google Vision and Microsoft Cognitive Services. The latter had the most convincing examples and a good documentation.&nbsp;With the free version you can make 20 calls a minute and up to 30.000 calls a month. Like most API&#8217;s you sent a REST call and you&#8217;ll get the data back as JSON.</p>
<p><a href="https://media.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api.jpg"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-2629" src="https://media.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api.jpg" alt="Emotion Detection with Azure and Processing" width="1280" height="720" srcset="https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api.jpg 1280w, https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api-400x225.jpg 400w, https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api-480x270.jpg 480w, https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api-768x432.jpg 768w, https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api-800x450.jpg 800w, https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/processing-azure-face-api-1200x675.jpg 1200w" sizes="(max-width: 1280px) 100vw, 1280px" /></a></p>
<p>Make sure you request the Azure Face API subscription key. <a href="https://azure.microsoft.com/en-us/try/cognitive-services/">On this page</a> you can make a trial API key, which works from the West-US server. However you can also <a href="https://docs.microsoft.com/en-us/azure/cognitive-services/cognitive-services-apis-create-account">create an Azure Account</a> and add the Face API over there (use the search function, because to interface is not really straight forward). That would give you the benefit of all the Azure servers and (I think) the free version is not limited to 30 days. &nbsp;</p>
<h3>Process</h3>
<p>I&#8217;ve based my Processing sketch on this <a href="https://docs.microsoft.com/en-us/azure/cognitive-services/face/quickstarts/java">Quickstart example for Java</a>. It uses the Apache HTTP client library (from the <a href="https://hc.apache.org">Apache HTTPComponents project</a>). There is an implementation for Processing called the <a href="https://github.com/runemadsen/HTTP-Requests-for-Processing">HTTP Requests for Processing</a> library. However not everything is implemented, so I decided to include the HTTPClient library directly in the Sketch.&nbsp;It&#8217;s actually pretty easy to include compiled Java (*.jar) code in Processing. I&#8217;ve created a folder named &#8220;code&#8221; in the Sketch folder and copied the files in there.&nbsp;</p>
<p><a href="https://media.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/Java-code-in-Processing-Sketch.png"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-2627" src="https://media.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/Java-code-in-Processing-Sketch.png" alt="Java code in Processing Sketch" width="759" height="313" srcset="https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/Java-code-in-Processing-Sketch.png 759w, https://kasperkamperman.com/media/blog/using-azure-cognitive-services-with-processing/Java-code-in-Processing-Sketch-480x198.png 480w" sizes="(max-width: 759px) 100vw, 759px" /></a></p>
<p>You can <a href="https://hc.apache.org/downloads.cgi">download the HttpClient binary over here</a>. However it&#8217;s already included in my example.&nbsp;</p>
<p>The <a href="https://docs.microsoft.com/en-us/azure/cognitive-services/face/quickstarts/java">Quickstart code</a> shows&nbsp;how to upload an online image, however I needed to upload a local file. This <a href="https://stackoverflow.com/questions/39541634/how-to-send-a-local-image-instead-of-url-to-microsoft-cognitive-face-api-using-j">StackOverflow post</a> showed how to do that with the FileEntity class.&nbsp;</p>
<h3>The Processing demo</h3>
<p>The demo code creates a picture out of the webcam stream (press space) and uploads this to the Azure Face API. It receives the data string in JSON format, which we then parse with the Processing JSON functions (<a href="https://processing.org/reference/JSONArray.html">JSONArray</a> and <a href="https://processing.org/reference/JSONObject.html">JSONObject</a>). A <a href="https://processing.org/reference/PGraphics.html">PGraphics canvas</a> is created to directly overlay parts of the output on our screenshot picture. I noticed this goes slower after the first data comes in (no idea why), however after that it goes kind of realtime. I didn&#8217;t parse all the data, <a href="https://westus.dev.cognitive.microsoft.com/docs/services/563879b61984550e40cbbe8d/operations/563879b61984550f30395236">check the reference</a> to see all the data you&#8217;ll get back.&nbsp;</p>
<p>I&#8217;ve implemented a timer to limit the calls to the API (to stay within the 20 calls per minute). The FaceAnalysis class runs as a separate thread. This is necessary otherwise the draw-loop would wait until the Azure service (or any web service) would sent back the requested information.&nbsp;There is the <a href="https://processing.org/reference/thread_.html">thread()</a> function in Processing, however that doesn&#8217;t work in classes. Luckily there was some basic information on how to use Threads from Daniel Shiffman on the Processing Wiki (now only accessible through the <a href="https://web.archive.org/web/20121204090925/http://wiki.processing.org/w/Threading">Wayback Machine</a>).&nbsp;</p>
<p><a class="fasc-button fasc-size-large fasc-type-flat ico-fa fasc-ico-before fa-github-alt" style="background-color: #000000; color: #ffffff;" href="https://github.com/kasperkamperman/Processing-FaceAPI">Get the code on Github</a></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>The post <a href="https://www.kasperkamperman.com/blog/using-azure-cognitive-services-with-processing/">Using Azure Cognitive Services with Processing</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Particle Photon (STM32F205) DMA Control of GPIO pins</title>
		<link>https://www.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-gpio-pins/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Tue, 16 Jan 2018 21:05:45 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2548</guid>

					<description><![CDATA[<p>In this article I explain how to you can control the GPIO pins of the Particle Photon with Direct Memory Access. This makes it for example easy to drive LED strips without any CPU cost. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-gpio-pins/">Particle Photon (STM32F205) DMA Control of GPIO pins</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<h3>Introduction</h3>
<p>For my light installations I work with different addressable LED strips like Dotstar (APA102) and WS2812 (Neopixels). You can control them by sending serial data (a bunch of 0&#8217;s and 1&#8217;s in a certain structure) to the data input. In <a href="https://cpldcpu.wordpress.com/2014/08/27/apa102/">Tim&#8217;s blog</a> you can read for example how APA102 pixels are controlled.</p>
<p>The &#8220;problem&#8221; with sending that data is that the CPU of the microcontroller is busy with that. You can&#8217;t do any other things in the mean time. The longer your LED pixel strip is the longer the wait. Besides that different chipsets have different data rates as you can read in the <a href="https://github.com/FastLED/FastLED/wiki/Chipset-reference">FastLED chipset reference</a>. The APA102&#8217;s can be updated super fast (although you need 2 wires, one for clock, one for data), but WS2812 chips are really slower.</p>
<p>One of the nice things of faster STM32 processors is that they have a DMA (Direct Memory Access) feature. This makes it possible to output data in memory to output pins without the need of the CPU. So while DMA functionality transfers data to your LEDstrip, you are able to render for example your effects with the CPU. Keep in mind that for most applications this is an overkill, you won&#8217;t need it.&nbsp;<span class="entry-author"><span class="entry-author-name"><a href="http://www.mind-dump.net/using-the-dma-controller-on-stm32f4">Andreas Finkelmeyer&nbsp;wrote a good explanatory post on DMA</a>.&nbsp;</span></span></p>
<p>Their are some great DMA implementations already for LEDstrips, the best example is the <a href="https://www.pjrc.com/teensy/td_libs_OctoWS2811.html">OctoWS2811 library by Paul Stoffregen (Teensy)</a>. For the <a href="https://www.particle.io/products/hardware/photon-wifi-dev-kit">Particle Photon</a> I&#8217;ve <a href="https://github.com/kasperkamperman/Particle-DotStar">released a modified version of the Adafruit DotStar library</a>, to control APA102 strips with DMA (thanks to the tips of <a href="https://github.com/pixelmatix/SmartMatrix-Photon-APA102">Louis Beaudoin</a> of the SmartMatrix library). This is possible, because for SPI transfers DMA is already implemented in the firmware. The limitation is that you have to use the SPI pins (A3/A5 and D2/D4) to connect your APA102/DotStar strips.</p>
<p>For a recent project I&#8217;m interested in controlling lights with DMX. Because DMX has slow dataspeed (compared with led pixel strips), the ability to send data in the background is kind of a need. SPI speed cannot set precisely enough, so I had to come with another solution. I&#8217;ve found several DMA examples, but without some knowledge of it&#8217;s inner workings, it&#8217;s pretty impossible to make it work. There is not a lot of explanation for people that don&#8217;t have an engineering background (like me), so it cost me a lot to understand this stuff. However I finally got a small prototype (just some blinking LEDs) to work, so I thought I might share my learnings for other engineering &#8220;noobs&#8221;.&nbsp;</p>
<p>In this proof of concept I transfer data from an array to a GPIO port. In this case just two blinking LED&#8217;s on the A3 and A5 ports. Because of a circular double buffer, this will continue without any CPU intervention.&nbsp;</p>
<p><a class="fasc-button fasc-size-medium fasc-type-flat ico-fa fasc-ico-before fa-github-alt" style="background-color: #33809e; color: #ffffff;" href="https://gist.github.com/kasperkamperman/ee518512a9cfbcfa402ef96d5a3050bd">DMA output concept on GIST</a></p>
<h2>GPIO registers and other stuff</h2>
<p>If you work with Arduino you are probably used to set pins to HIGH and LOW with functions like:</p>
<pre class="lang:default decode:true">digitalWrite(13, HIGH);
digitalWrite(13, LOW);</pre>
<p>If you need to send Serial data (bit banging), these functions are pretty slow (we are talking about microseconds now). They need to work on a lot of different devices (the Arduino script language Wiring is supported by many platforms). In order to manipulate a pin in a faster way we have to work on a lower level. Particle has some other functions like <a href="https://docs.particle.io/reference/firmware/photon/#pinsetfast-">pinSetFast()</a> and <a href="https://docs.particle.io/reference/firmware/photon/#pinresetfast-">pinResetFast()</a>. However this won&#8217;t work for DMA, you need to access the GPIO register through a port address directly.&nbsp;</p>
<p>In embedded programming a pin is called a GPIO (General Purpose Input Output) pin. Those pins are connected to a port.</p>
<p><a href="https://media.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/photon-pinout1.png"><img loading="lazy" decoding="async" class="alignnone wp-image-2549 size-full" src="https://media.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/photon-pinout1.png" alt="Particle Photon pinout" width="776" height="326" srcset="https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/photon-pinout1.png 776w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/photon-pinout1-480x202.png 480w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/photon-pinout1-768x323.png 768w" sizes="(max-width: 776px) 100vw, 776px" /></a></p>
<p>As you can see in this pinout diagram from the <a href="https://docs.particle.io/datasheets/photon-(wifi)/photon-datasheet/#pinout-diagrams">Particle Photon datasheet</a>&nbsp;pin A0 is connected to port C pin5, and pin A3 is connected to port A, pin5. Internally a <a href="https://github.com/particle-iot/firmware/blob/develop/hal/src/stm32f2xx/pinmap_hal.c#L86">pin map</a> is used to make sure that A0 is actually controlling pin 5 on port C (which is of course still labeled as A0 on the physical device). <a href="https://github.com/particle-iot/firmware/blob/develop/hal/src/stm32f2xx/pinmap_hal.c#L86">This pin map code</a>&nbsp;is also a great resource to understand how GPIO ports and pins are used.</p>
<p>In order to set certain GPIO pins to HIGH and LOW you can write to the pin registers. ScruffR explains that in the <a href="https://community.particle.io/t/parallel-gpio-direct-port-write-access-for-photon/23628/3">Particle forum</a>.</p>
<blockquote><p>And for direct port writes you can use this:</p>
<pre><code>    GPIOA-&gt;ODR = 0xAAAA;    // directly setting the respective pins HIGH or LOW
    GPIOA-&gt;BSRRL = 0xAAAA;  // only setting the pins with a set bit to HIGH (counter intuitive tho')</span>
    GPIOA-&gt;BSRRH = 0x5555;  // only resetting the pins with a set bit to LOW (counter intuitive tho')</span></code></pre>
<p>There would also be an atomic instruction to have BSRRH &amp; BSRRL set at once, but <code>_IO uint32_t BSRR</code> is not declared for some reason, but this workaround might work (not yet tested tho&#8217;)</p></blockquote>
<p>Each port has 16 pins (16 bit), so each bit sets or resets a certain pin.</p>
<p>ODR we don&#8217;t want to use, because it influences all the pins on the port (they will be reset automatically). Since I&#8217;d like to keep using other pins (like SPI, D7 etc) function just as normal, this is not an option.&nbsp;</p>
<p>Julien Vanier came with <a href="https://community.particle.io/t/ws2812-dma-library-resolved/2919/37">the suggestion</a> to use the BSRRL and BSRRH registers and use two DMA channels to control them. However BSRRL and BSRRH actually form together the 32 bit set/unset register named BSRR. So the &#8220;left&#8221; 16 bit part of the number will reset the pins, the &#8220;right&#8221; 16 bit part will set the pins.&nbsp;</p>
<p>Using the BSRR register will save us a DMA channel (and Timers that have to be synced).&nbsp;One small problem is that BSRR is not declared in the Particle firmware. However the address of BSRRL points to the same location, so the workaround of ScruffR works.</p>
<p>Below (<a href="https://go.particle.io/shared_apps/5a5672a8faa63810a900070c">or in the Particle IDE here</a>) an example of blinking the D7 LED with GPIO manipulation of BSRRL, BSRRH and through BSRR.&nbsp;</p>
<pre><code>
// D7 is GPIO pin 13 on GPIOA. 
// https://github.com/particle-iot/firmware/blob/develop/hal/src/stm32f2xx/pinmap_hal.c#L80

void setup() {
    pinMode(D7, OUTPUT);
}

void loop() {
    
    //GPIOA-&gt;BSRR  = 0b00000000000000000010000000000000; // HIGH
    GPIOA-&gt;BSRRL = 0b0010000000000000; // HIGH
    
    delay(1000);
    
    //GPIOA-&gt;BSRR = 0b00100000000000000000000000000000; // LOW
    GPIOA-&gt;BSRRH = 0b0010000000000000; // LOW
    
    delay(1000);
    
    // actually we can make the "left" part by using the "right" BSSRL part and using a 
    // XOR, a pinMask and a bitshift of &lt;&lt;16 
    
    // 0b0010000000000000 HIGH
    // 0b0010000000000000 pinMask
    // ------------------ XOR
    // 0b0000000000000000
    
    // 0b0000000000000000 LOW
    // 0b0010000000000000 pinMask
    // ------------------ XOR
    // 0b0010000000000000
    
    // if we use BSRR we can use a 1 for HIGH and a 0 for LOW by using XOR and a pinMask. 
    uint16_t pinMaskD7 = 1&lt;&lt;13; // pin 13 on GPIOA (same as 0b0010000000000000)
    
    uint16_t valueHigh = 0b0010000000000000;
    uint16_t valueLow  = 0;
    
    uint32_t* GPIOA_BSRR = (uint32_t*)&amp;GPIOA-&gt;BSRRL;
    
    //GPIOA-&gt;BSRR = valueHigh + ((valueHigh ^ pinMaskD7) &lt;&lt; 16);
    *GPIOA_BSRR = valueHigh + ((valueHigh ^ pinMaskD7) &lt;&lt; 16);
    
    delay(1000);
    
    //GPIOA-&gt;BSRR = valueLow + ((valueLow ^ pinMaskD7) &lt;&lt; 16);
    *GPIOA_BSRR = valueLow + ((valueLow ^ pinMaskD7) &lt;&lt; 16);
    
    delay(1000);
    
}</code></pre>
<p>This&nbsp;<a href="http://hertaville.com/stm32f0-gpio-tutorial-part-1.html">GPIO tutorial for the STM32 Discovery board</a>&nbsp; helped me understand how GPIO registers and BSRR works.</p>
<h3>Transfer data to output pins with DMA.</h3>
<p>Like mentioned we would like to modify the GPIO registers with DMA. Each processor has it&#8217;s own architecture with Timers, GPIO&#8217;s and DMA channels/streams. If found different examples that used DMA to controlled pins or elements. A great working example for the Particle Photon was the <a href="https://github.com/monkbroc/particle-speaker">Particle Speaker library</a>&nbsp;by Julien Vanier. It uses DMA to control the DAC pin (A7 on the Photon) to play sinus waves. However my idea, was not to be limited to that pin. I wanted to be able to control any pin with DMA.</p>
<p>I started reverse engineering the code, understanding what different lines of code do. The biggest question was why&nbsp;DMA1 Stream5, channel7 is used. Because changing those numbers, would break the code. Well the answer can be find in Table 22 of the reference manual for the <a href="http://www.st.com/content/ccc/resource/technical/document/reference_manual/51/f7/f3/06/cd/b6/46/ec/CD00225773.pdf/files/CD00225773.pdf/jcr:content/translations/en.CD00225773.pdf">STM32F20x range (RM0033)</a>.</p>
<p><a href="https://media.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping.png"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-2551" src="https://media.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping.png" alt="" width="2192" height="928" srcset="https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping.png 2192w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping-480x203.png 480w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping-768x325.png 768w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping-800x339.png 800w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-22-DMA1-request-mapping-1200x508.png 1200w" sizes="(max-width: 2192px) 100vw, 2192px" /></a></p>
<p>As you can see above. Channel 7 of DMA, Stream 5 is mapped to DAC1. You can also see that certain channels/streams are connected to a Timer. In other examples I&#8217;ve found that this timer needs to match with an DMA Stream/Channel.</p>
<p>Another important insight came from this <a href="https://stackoverflow.com/questions/46613053/pwm-dma-to-a-whole-gpio/46619315#46619315">post on StackOverflow</a>:</p>
<blockquote><p>There is a problem though, that <code>DMA1</code> cannot access the AHB bus at all (see Fig. 1 or 2 in the Reference Manual), to which the GPIO registers are connected. Therefore we must use <code>DMA2</code>, and that leaves us with the advanced timers <code>TIM1</code> or <code>TIM8</code>.</p></blockquote>
<p><a href="https://media.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping.png"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-2552" src="https://media.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping.png" alt="" width="2178" height="1074" srcset="https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping.png 2178w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping-480x237.png 480w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping-768x379.png 768w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping-800x394.png 800w, https://kasperkamperman.com/media/blog/particle-photon-stm32f205-dma-control-of-gpio-pins/Table-23-DMA2-request-mapping-1200x592.png 1200w" sizes="(max-width: 2178px) 100vw, 2178px" /></a></p>
<p>Since TIMER1 is used on the Particle for other <a href="https://community.particle.io/t/which-timers-are-used-by-the-photon-firmware-libraries/27741">functions</a>, only TIMER8 can be used to control the DMA transfer.</p>
<p>Just as in the Speaker library a double circular buffer is used. This means that you can update one buffer, while another buffer is transferred. This makes it possible to transmit DMX on a fixed frame rate in the background.&nbsp;</p>
<p>Like mentioned at the beginning of this post you can check the proof of concept on GIST. It works with GPIOA (A4,A5,A6,A7,D5,D6,D7) but you can also select GPIOC (A0, A1, A30 or GPIOB (D0,D1,D2,D3,D4).&nbsp;</p>
<p><a class="fasc-button fasc-size-medium fasc-type-flat ico-fa fasc-ico-before fa-github-alt" style="background-color: #33809e; color: #ffffff;" href="https://gist.github.com/kasperkamperman/ee518512a9cfbcfa402ef96d5a3050bd">DMA output concept on GIST</a></p>
<pre class="">#include "Particle.h"

// RM0033 MANUAL - Table 23 / Figure 1 System architecture
// Photon is STM32F205
// Only DMA2 is connected with GPIO ports https://stackoverflow.com/questions/46613053/pwm-dma-to-a-whole-gpio
// GPIO BSSRL/BSSRH/BSSR http://hertaville.com/stm32f0-gpio-tutorial-part-1.html
// DMA_Mode_Circular https://github.com/monkbroc/particle-speaker
// Ulrich Radig OctoArtnetNode https://www.ulrichradig.de/home/index.php/dmx/8-kanal-art-net
// Thanks to Julien Vanier for the idea of BSRR manipulation.

// The timers connected to APB2 are clocked from TIMxCLK up to 120 MHz
// In case of the Photon: TIM1, TIM8.

//SYSTEM_MODE(MANUAL); // only use this when you build local

// D7 is GPIO pin 13 on GPIOA.
uint16_t pinMask = 1&lt;&lt;13; // pin 13 on GPIOA (same as 0b0010000000000000)

uint16_t bufferSize = 4;
uint16_t blink_turnhigh_buffer[4];
uint32_t blink_bsrr_buffer0[4];
uint32_t blink_bsrr_buffer1[4]; // double buffer

void timerInit();
void dmaInit();

void setup() { // Put setup code here to run once

    delay(2000);
    Serial.begin(57600);

    timerInit();
    dmaInit();

    pinMode(D7, OUTPUT);

    //WiFi.off();

    // D7 is GPIO pin 13 on GPIOA.
    // of course you can modify multiple pins if you want
    // for example 0b0000000010100000 would turn on A3 and A5 (don't forget to modify the pinMask);
    blink_turnhigh_buffer[0] = 1&lt;&lt;13; //(same as 0b0010000000000000)
    blink_turnhigh_buffer[1] = 0;
    blink_turnhigh_buffer[2] = 1&lt;&lt;13;
    blink_turnhigh_buffer[3] = 0;

    // We use a XOR operation to mask and make the turn to LOW register low
    // (Done by sending a one. )
    for(int i = 0; i &lt; 4; i++) {

      blink_bsrr_buffer0[i] = blink_turnhigh_buffer[i] + ((blink_turnhigh_buffer[i] ^ pinMask) &lt;&lt; 16);
      blink_bsrr_buffer1[i] = blink_bsrr_buffer0[i]; // double buffer

    }
}

void loop() {

}

void timerInit (void) {

  // https://github.com/pkourany/SparkIntervalTimer/blob/master/src/SparkIntervalTimer.cpp
  // tryout with a slow timer. 2Hz (so each number is 0.5ms)
  //const uint16_t SIT_PRESCALERu = (uint16_t)(SYSCORECLOCK / 1000000UL) - 1;	//To get TIM counter clock = 1MHz
  const uint16_t SIT_PRESCALERm = (uint16_t)(SystemCoreClock / 2000UL) - 1;	  //To get TIM counter clock = 2KHz

  TIM_TimeBaseInitTypeDef	TIM_TimeBaseStructure;

  TIM_DeInit(TIM8);

  RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE);

  TIM_TimeBaseStructure.TIM_Prescaler = SIT_PRESCALERm;
  //TIM_TimeBaseStructure.TIM_Prescaler = SIT_PRESCALERu;
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
  TIM_TimeBaseStructure.TIM_Period =  2000;
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
  TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;

  TIM_TimeBaseInit(TIM8,&amp;TIM_TimeBaseStructure);
  TIM_ClearFlag(TIM8,TIM_FLAG_Update);

  TIM_Cmd(TIM8, ENABLE);

}

void dmaInit(void) {

  // DMA2 only connects to GPIO ports...
  // DMA2 channel 7 stream 1 connects to TIM8_UP
  DMA_InitTypeDef DMA_InitStructure;

  // Clock enable
  RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);

  DMA_Cmd(DMA2_Stream1, DISABLE);
  DMA_DeInit(DMA2_Stream1);

  DMA_StructInit(&amp;DMA_InitStructure);

  DMA_InitStructure.DMA_Channel = DMA_Channel_7;
  DMA_InitStructure.DMA_PeripheralBaseAddr = ((uint32_t)&amp;(GPIOA-&gt;BSRRL));
  DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t) blink_bsrr_buffer0;
  DMA_InitStructure.DMA_BufferSize = bufferSize;

  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;

  DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;

  /* Configure double buffering */
  DMA_DoubleBufferModeConfig(DMA2_Stream1, (uint32_t) blink_bsrr_buffer1, DMA_Memory_1);
  DMA_DoubleBufferModeCmd(DMA2_Stream1, ENABLE);

  DMA_Init(DMA2_Stream1, &amp;DMA_InitStructure);

  DMA_Cmd(DMA2_Stream1, ENABLE);

  // DMA-Timer8 enable
  TIM_DMACmd(TIM8,TIM_DMA_Update,ENABLE);

}</pre>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>The post <a href="https://www.kasperkamperman.com/blog/particle-photon-stm32f205-dma-control-gpio-pins/">Particle Photon (STM32F205) DMA Control of GPIO pins</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to add source links to image captions in WordPress</title>
		<link>https://www.kasperkamperman.com/blog/source-link-captions-in-wordpress/</link>
					<comments>https://www.kasperkamperman.com/blog/source-link-captions-in-wordpress/#comments</comments>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Wed, 08 Nov 2017 15:11:42 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2376</guid>

					<description><![CDATA[<p>I needed to add some illustrative pictures to a blog article and I wanted to give proper credit to the photographers and link to their websites. I didn't found a proper plugin that I liked so I created this solution. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/source-link-captions-in-wordpress/">How to add source links to image captions in WordPress</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>If you are adding an image (or other media file) to the WordPress Media library you can set certain meta-data, like Caption, Alt-Text and Description. I wanted to make part of the Caption a clickable link that refers to the source/website of the picture to give proper credits to the original author. Unfortunately this is not really a common habit in the blog-o-sphere I&#8217;ve noticed (trying to find the sources of one original image), so it made even more sense to me to create a solution for proper attribution.</p>
<p>The most easy might be to switch to Text view and add some anchor text around the part you want to link. However I liked a more user friendly solution in which the source is stored within the Media Library itself.</p>
<p>I started out trying some plugins like <a href="https://wordpress.org/plugins/media-credit/">media-credit</a> and <a href="https://wordpress.org/plugins/better-image-credits/">better image credits</a>. However they added too much stuff around it, while I essentially wanted hyper-linked caption.</p>
<p>Luckily I came across a solution from <a href="https://kaspars.net/blog/wordpress/how-to-automatically-add-image-credit-or-source-url-to-photo-captions-in-wordpress">Kaspars Dambis; How to Automatically Add Image Credit or Source URL to Photo Captions in WordPress</a>. It behaved a bit strange with https links, but it was a solid base to work on. He created a filter for the &#8216;img_caption_shortcode&#8217;. You can read more about how Captions work in <a href="http://justintadlock.com/archives/2011/07/01/captions-in-wordpress">Justin Tadlock&#8217;s blog</a>.</p>
<h2>Solution</h2>
<p><a href="https://media.kasperkamperman.com/blog/how-to-add-source-links-to-image-captions-in-wordpress/Screen-Shot-2017-11-08-at-14.42.30.png"><img loading="lazy" decoding="async" class="alignright wp-image-2379 size-medium" src="https://media.kasperkamperman.com/blog/how-to-add-source-links-to-image-captions-in-wordpress/Screen-Shot-2017-11-08-at-14.42.30-257x480.png" alt="" width="257" height="480" srcset="https://kasperkamperman.com/media/blog/how-to-add-source-links-to-image-captions-in-wordpress/Screen-Shot-2017-11-08-at-14.42.30-257x480.png 257w, https://kasperkamperman.com/media/blog/how-to-add-source-links-to-image-captions-in-wordpress/Screen-Shot-2017-11-08-at-14.42.30-429x800.png 429w, https://kasperkamperman.com/media/blog/how-to-add-source-links-to-image-captions-in-wordpress/Screen-Shot-2017-11-08-at-14.42.30.png 574w" sizes="(max-width: 257px) 100vw, 257px" /></a></p>
<p>Most of the solution was already provided by Kaspars Dambis. I&#8217;ve added an extra field below the Source URL field in which you can fill in the credits. If you fill in the source URL only, the whole text in the Caption will change in a hyperlink to the source. If you fill in the credits field, that will be added to the  caption (after a &#8216;-&#8216; symbol) as a hyperlink.</p>
<p>To use this, just copy-paste <a href="https://gist.github.com/kasperkamperman/2110c61b55a5d3b2f4a319a00afdef0f">the code</a> to the functions.php of the theme (or child-theme) that you are using.</p>
<h2>Result</h2>
<p>Check the result of the plugin in <a href="https://www.lumiflow.nl/blog/artikelen/dynamische-verlichting/">my article on dynamic light</a> (in Dutch only) at the <a href="https://www.lumiflow.nl">Lumiflow website</a>.</p>
<h2>Code</h2>
<p>View the code on <a href="https://gist.github.com/kasperkamperman/2110c61b55a5d3b2f4a319a00afdef0f">Gist</a>.</p>
<p>The post <a href="https://www.kasperkamperman.com/blog/source-link-captions-in-wordpress/">How to add source links to image captions in WordPress</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.kasperkamperman.com/blog/source-link-captions-in-wordpress/feed/</wfw:commentRss>
			<slash:comments>8</slash:comments>
		
		
			</item>
		<item>
		<title>Penner Easing functions in Processing</title>
		<link>https://www.kasperkamperman.com/blog/penner-easing-functions-processing/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Mon, 03 Jul 2017 13:34:03 +0000</pubDate>
				<category><![CDATA[Codelog]]></category>
		<category><![CDATA[Processing]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2134</guid>

					<description><![CDATA[<p>Demonstration of the raw Robert Penner easing functions implemented in the Ani library for Processing. </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/penner-easing-functions-processing/">Penner Easing functions in Processing</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><a href="https://media.kasperkamperman.com/blog/easing/easing.gif"><img loading="lazy" decoding="async" class="alignnone size-full wp-image-2135" src="https://media.kasperkamperman.com/blog/easing/easing.gif" alt="Easing in Processing" width="640" height="480" /></a></p>
<p>I needed to use the raw <a href="http://robertpenner.com/easing/">Robert Penner easing functions</a>. The functions are already implemented in the <a href="http://benedikt-gross.de/libraries/Ani/">Ani Library from Benedikt Gross</a>, but there wasn&#8217;t an example to use the functions directly without using the transition and time function. Apart from timed animations you could use the functions as well as a curve in audio-visualisations (for example the Sound library can give you back an amplitude on which you can apply these functions).</p>
<p>In the code below I use the object, but syntax like this also works:</p>
<pre class="lang:java decode:true">float easingOutput = AniConstants.CUBIC_IN_OUT.calcEasing(normalizedValue,0.0,1.0,1.0);</pre>
<p>In this example code I used normalized values.  Which means that we use an value between 0.0 &#8211; 1.0 as input and also get that back as output.</p>
<p>The functions accept 4 values:</p>
<ul>
<li>t is time, in this case just 0.0 &#8211; 1.0</li>
<li>b is the start, we just use 0.0</li>
<li>c is the change, we use normalized so also 1.0</li>
<li>d is the duration, because we use normalised it&#8217;s 1.0</li>
</ul>
<p>If you only need the functions you can also copy-paste them from the <a href="https://github.com/b-g/Ani/tree/master/src/de/looksgood/ani/easing">Ani-library source</a>.</p>
<pre class="lang:java decode:true " title="Easing-demo.pde">import de.looksgood.ani.AniConstants;
import de.looksgood.ani.easing.*;
import de.looksgood.ani.*;

Sine sine = new Sine();
Quad quad = new Quad();
Cubic cubic = new Cubic()
Quart quart = new Quart();
Quint quint = new Quint();
Expo expo = new Expo();
Circ circ = new Circ();
Back back = new Back();
Elastic elastic = new Elastic();
Bounce bounce = new Bounce();

// stuff it all in an array for display

Easing[] easingFunctions = { 
  sine, quad, cubic, quart, quint, expo, circ, back, elastic, bounce
};

String [] easingNames = {
  "sine", "quad", "cubic", "quart", "quint", "expo", "circ", "back", "elastic", "bounce"
};

String [] aniModes = { "IN", "OUT", "IN_OUT" };
int currentAniMode = AniConstants.IN_OUT;

float xLeftMargin  = 80;
float xRightMargin = 80;

void setup() {
  // tip FX2D looks really smooth on retina displays
  // some things won't work in this mode, but most things do
  size(640,480,FX2D);
  noStroke();
  frameRate(25);
  
  changeMode(currentAniMode);
}

void draw() {
  
  background(255);
  
  float durationInMilliSeconds = 4000;
  float normalizedValue        = (millis()%durationInMilliSeconds)/durationInMilliSeconds;
  
  // overwrite the calculated value when pressing the mouse
  if(mousePressed) normalizedValue = mouseX/(float)width;
  
  fill(0);
  text("easing mode : " + aniModes[currentAniMode] + " - " + nf(normalizedValue,0,2),10,20);
  
  fill(224);
  rect(xLeftMargin,40,(width-xLeftMargin-xRightMargin),height-80);
  
  // line to display the linear 0.0 - 1.0 progress
  stroke(196);
  float lineX = xLeftMargin + (normalizedValue * (width-xLeftMargin-xRightMargin));
  line(lineX,40,lineX,height-40);
  
  for(int i = 0; i &lt; easingFunctions.length; i++) {
    
    float yPos = 60+(i*40);
    
    float easingOutput = easingFunctions[i].calcEasing(normalizedValue,0.0,1.0,1.0);
    
    // put the text in front and draw horizontal line
    fill(0);
    text(easingNames[i], 20, yPos);
    line(xLeftMargin,yPos,(width-xRightMargin),yPos);
    
    // draw the eased circle
    drawCircle(easingOutput, yPos, 20);
    
  }
  
  text("Press to mouse control the value manually. Change the Ease mode with the 1, 2, 3 keys.", 20, height-15);
  
  if(frameCount &gt; 22 &amp;&amp; frameCount&lt;151) saveFrame("easing-#####.png");
  
}

void drawCircle(float normalizedValue, float yPos, float radius) {
  // calculate the x position and take the margins in account
  float xPos = xLeftMargin + (normalizedValue * (width-xLeftMargin-xRightMargin));
  noStroke();
  fill(64);
  ellipse(xPos,yPos,radius,radius);
}


void keyReleased() {
  
  if(key == '1') {
    changeMode(AniConstants.IN);
   }
  else if(key == '2') {
    changeMode(AniConstants.OUT);
  }
  if(key == '3') {
    changeMode(AniConstants.IN_OUT);
  }
  
}

void changeMode(int mode) {
  
  currentAniMode = mode;
  // change the mode for all easing objects
  for (Easing e : easingFunctions) {
     e.setMode(currentAniMode); 
  }
}</pre>
<p>&nbsp;</p>
<p>The post <a href="https://www.kasperkamperman.com/blog/penner-easing-functions-processing/">Penner Easing functions in Processing</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Particle Photon RGB Cloud Remote Control</title>
		<link>https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Fri, 02 Jun 2017 08:33:44 +0000</pubDate>
				<category><![CDATA[Arduino]]></category>
		<category><![CDATA[Codelog]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2048</guid>

					<description><![CDATA[<p>In this demo I show you how to create a simple JavaScript based web application that can communicate with the Particle Photon. With the app you can control color of the RGB led on the Photon with Hue, Saturation and Brightness sliders. The HSB>RGB conversion is done on the Photon and is also send back to the app to change the background of the title. You don’t need extra sensors, just a plain Particle Photon board.</p>
<p>The post <a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/">Particle Photon RGB Cloud Remote Control</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>I’ve developed this demo to demonstrate the communication speed of the <a href="https://www.particle.io/products/platform/particle-cloud">Particle Cloud</a>. This is fast but not always fast enough for realtime communication. In the <a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-local/">Particle Photon Local Remote Control article</a> I explain how you can speed up communication by bypassing the Particle Cloud. We do that by running a simple&nbsp;web server on the Photon.&nbsp;</p>
<p><a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/"><img decoding="async" src="https://www.kasperkamperman.com/wordpress_kk/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=https%3A%2F%2Fi.ytimg.com%2Fvi%2FcAb57_Jmu3o%2Fhqdefault.jpg" alt="YouTube Video"></a><br /> <a href="https://youtu.be/cAb57_Jmu3o" target="_blank">Watch this video on YouTube</a>.</p>
<h3>Techniques used in Web App</h3>
<ul>
<li>Doing AJAX GET and POST requests with the native (<a href="http://vanilla-js.com">Vanilla JS</a>) <a href="https://www.w3schools.com/xml/dom_http.asp">XMLHttpRequest</a> function.</li>
<li>Different checks (404, Particle.connected).</li>
<li>Rate limit (<a href="https://css-tricks.com/debouncing-throttling-explained-examples/">throttle</a>) requests.</li>
<li>Passing values as a comma separated string.</li>
<li>Parsing received <a href="https://www.w3schools.com/js/js_json_intro.asp">JSON</a> data.</li>
<li>Calculating performance of the <a href="https://docs.particle.io/reference/firmware/photon/#particle-function-">Particle Function</a>.</li>
<li><a href="https://getmdl.io">Material Design Lite</a> as framework.</li>
</ul>
<h3>Techniques used on the Photon</h3>
<ul>
<li>Using <a href="https://docs.particle.io/reference/firmware/photon/#particle-function-">Particle Function</a> and <a href="https://docs.particle.io/reference/firmware/photon/#particle-variable-">Particle Variable</a></li>
<li>Implemented <a href="https://www.kasperkamperman.com/blog/arduino/arduino-programming-hsb-to-rgb/">HSB to RGB conversion</a> and <a href="https://gist.github.com/kasperkamperman/3c3f72208366ed885f2f">brightness correction LUT</a>.</li>
<li>Parsing a comma separated string.</li>
<li>Formatting a <a href="https://www.w3schools.com/js/js_json_intro.asp">JSON string</a>.</li>
<li><a href="https://docs.particle.io/reference/firmware/photon/#rgb">Control the RGB led on the Photon</a>.</li>
</ul>
<h3>What happens</h3>
<div>In this example we send 4 values (hue, saturation, brightness and time) as a comma separated string to a Particle Function. The function will return the same time again as an integer (a Particle Function can only receive strings and return integers). By checking the difference between the time and the current time, we know how much time this roundtrip took.&nbsp;If we get the result of the function (the integer with the time), we do another call to a Particle.variable, which will give us the red, green and blue values as a JSON string. We don’t measure the time of round-trip, because you cannot pass data if you request a variable.</div>
<div>&nbsp;</div>
<div>To limit the amount of data send to the Particle Cloud I&#8217;ve implemented a Throttle/rate limit function (<a href="https://css-tricks.com/debouncing-throttling-explained-examples/">check out this good visualisation</a>). By default 2 messages a second are sent to the cloud (actually 4 because we call both the Particle Function and the Particle Variable). If you remove the rate limit, you see that communication slows down because Particle rate limits requests and most browsers only support up to 7 open requests. &nbsp;&nbsp;</div>
<h3>Flow</h3>
<ul>
<li>At start-up check if the device is connected by doing a call to the device and look for the connected variable.</li>
<li>When you move a slider or press the button we submit the Hue, Saturation and Brightness (and time) as a comma separated String to the setHSB Particle.function.</li>
<li>The Photon will display the color and the function will return the send time as an integer.</li>
<li>When we receive the response we calculated the time it took and display that in the durationChip.</li>
<li>We do a call to the getRGB Particle.variable to get the calculated red, green and blue values.</li>
<li>The Photon will return a JSON formatted string with r,g,b variables.</li>
<li>We change the title background based on the received values.</li>
</ul>
<h3>Known limitations/problems</h3>
<p>You need to set your deviceId and accessToken in the JavaScript code. It’s not smart to publish this on the internet, because then anyone can take ownership if your Photon. Just use this code on a private network or from your own computer. If you want a more sophisticated way to connect to your Photon (with username/pass), then this excellent <a href="https://community.particle.io/t/web-page-sample-code-using-particle-api-js/21590">Particle API JS tutorial of Rickkas7</a> will help you to get started.&nbsp;</p>
<p>The <a href="http://www.html5tutorial.info/html5-range.php">HTML5 range-slider input</a> type is used. I’ve found that it doesn’t work good on Touch devices. There are several solutions. I&#8217;ve added <a href="https://rangetouch.com">rangetouch.js</a>, however that doesn’t&nbsp;seem to have much effect right now. Maybe it conflict with the Material Design Light code.</p>
<p>The <a href="https://getmdl.io">Material Design Lite framework</a> is not actively developed anymore. However I liked the well-designed user interface elements for this demo.</p>
<h3>Design choices</h3>
<p>You might wonder&nbsp;why I use a comma-separated string to send data from JavaScript to the Photon and why&nbsp;I use a JSON formatted string to send data the other way round. I decided that it was best to use the parsing strengths of each platform. Sending a lot of data to an embedded platform is not a smart idea (although processors get faster), the less data the better. A comma-separated string feels like a balance between shortness and human-readability. It simpler than JSON, because you don’t pass the variable names. In case you would like to parse some JSON, this <a href="https://bblanchon.github.io/ArduinoJson/">Arduino JSON library</a> might be a good library&nbsp;to start.</p>
<p>I’ve decided to send a JSON string back from the Photon. Actually this is just more out of laziness, because parsing JSON is already build in JavaScript. I’ve kept the string as short as possible (so using r,g,b instead of red, green, blue).</p>
<p>As you can read you need both a Particle Function and a Particle Variable. The Particle Function to send a string to the Photon and the Particle variable to receive a string. Unfortunately there isn’t a type that receives a string and can pass back as string. (You could encode the rgb value in the integer if you really want, this seemed unnecessary for this demo).</p>
<p>In my other demo that communicates with the localhost and send data back and forth in one step by using a <a href="https://www.w3schools.com/xml/ajax_xmlhttprequest_send.asp">POST request</a>. We send a comma-separated string and we receive a JSON string back. You’ll notice it goes a lot faster, because we only need one call and more important because it stays on the local network.</p>
<p><a class="fasc-button fasc-size-large fasc-type-flat ico-fa fasc-ico-before fa-github-square" style="background-color: #000000; color: #ffffff;" href="https://github.com/kasperkamperman/Particle-RGB-RemoteControl">Check the code and Quickstart on GitHub</a></p>
<p>The post <a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/">Particle Photon RGB Cloud Remote Control</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Particle Photon RGB Local Remote Control</title>
		<link>https://www.kasperkamperman.com/blog/arduino/particle-photon-rgb-remote-local/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Fri, 02 Jun 2017 08:31:28 +0000</pubDate>
				<category><![CDATA[Arduino]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2046</guid>

					<description><![CDATA[<p>In this demo we use local communication to communicate with the Photon. This is a lot faster then communication through the Particle Cloud. Sending a message takes about 20ms instead of 300ms (and thats the timing of the Particle function and excluded the timing of passing the variable for the color). </p>
<p>The post <a href="https://www.kasperkamperman.com/blog/arduino/particle-photon-rgb-remote-local/">Particle Photon RGB Local Remote Control</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>This demo builds further on the code explained in the&nbsp;<a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/">Particle Photon RGB Cloud Remote Control article</a>. So it might be a good idea to read that first.</p>
<p><a href="https://www.kasperkamperman.com/blog/arduino/particle-photon-rgb-remote-local/"><img decoding="async" src="https://www.kasperkamperman.com/wordpress_kk/wp-content/plugins/wp-youtube-lyte/lyteCache.php?origThumbUrl=https%3A%2F%2Fi.ytimg.com%2Fvi%2FcAb57_Jmu3o%2Fhqdefault.jpg" alt="YouTube Video"></a><br /> <a href="https://youtu.be/cAb57_Jmu3o" target="_blank">Watch this video on YouTube</a>.</p>
<p>The idea is to run a small webserver on the Photon.&nbsp;Unfortunately there isn’t yet an HTTP server available in the Particle API. Maybe it will come a the near future, because WICED (the Broadcom SDK for the microchip on the Particle Photon) implements a HTTP server (Adafruit implemented this <a href="https://learn.adafruit.com/introducing-the-adafruit-wiced-feather-wifi/adafruithttp">HTTP-API</a>&nbsp;for their <a href="https://learn.adafruit.com/introducing-the-adafruit-wiced-feather-wifi/overview">WICED feather WiFi</a>). For the <a href="https://docs.particle.io/reference/firmware/photon/#softap-http-pages">SoftAP configuration page</a> this is already used, but that only works in Listen mode.</p>
<p>Luckily there is the <a href="https://github.com/kasperkamperman/Webduino">Webduino library</a>.&nbsp;It makes use of the existing <a href="https://docs.particle.io/reference/firmware/photon/#tcpserver">TCPServer</a> and <a href="https://docs.particle.io/reference/firmware/photon/#tcpclient">TCPClient</a> functions on the Photon and adds the HTTP layer on top of that.&nbsp;I&#8217;ve forked and updated the library ported by <a href="https://github.com/m-mcgowan/Webduino">Matthew McGowan</a>, because I noticed that in the previous version communication was slowed-down because of some kind of bug in the Particle Core. To my surprise it took 300 milliseconds&nbsp;to send a message on my local network almost the same time as it took when communicating over the Particle Cloud. My expectation was that it should be way faster. I&#8217;ve found the cause the code, an added delay which was added to solve a bug on the Particle Core. I&#8217;ve removed the added delay and the communication speed changed to 15-20 milliseconds&nbsp;for one message.&nbsp;</p>
<p>In my experience the Webduino library is not always stable. It doesn’t manage client/server connections. <a href="https://gist.github.com/rickkas7/481dab421ba3959a82f17d64be731f9a">Rickkas7 has some code</a> that does this better, with some modifications this might be a better and more stable alternative.&nbsp;</p>
<h3>Use Case</h3>
<p>The idea is to be able to directly control Photon by sending data over the local network. There are some implementations that serve a complete webpage from the Photon. The problem is that&nbsp;serving HTML, CSS and JavaScript&nbsp;&nbsp;(I’m not even talking about images) eats up a lot of memory space on your Photon.</p>
<p>What I like is that anyone can control the Photon. So you also don&#8217;t need to put your credentials. I&#8217;m designing a light product, and I want that people within the local network are able to change the color. I don&#8217;t even what that people need to download an app (if you opt for that, I&#8217;d advice to look into UDP). &nbsp;With the <a href="https://github.com/mrhornsby/spark-core-mdns">mDNS library</a> you can publish your device with an address in your network, like myphoton.local. This doesn&#8217;t seem to work out-of-the-box on Windows (if you have tested this, please share some info), however installing iTunes (Bonjour protocol of Apple is mDNS) seems to help. AJ ONeal did <a href="https://daplie.com/articles/mdns-scanning-your-home-network/">a good explanation on mDNS</a> of you&#8217;d like to know more.&nbsp;Another way to reach your device is to go to the ip-address in your browser like http://192.168.0.106/.</p>
<h3>Design choices</h3>
<p>My code builds on the idea of the&nbsp;<a href="https://www.hackster.io/wgbartley/iot-device-management-with-mdns-and-webduino-93982a">Hackster article of Garret Bartley</a>. He serves a webpage on the Photon with an iFrame. The iFrame points to an online webpage, served from a hosting&nbsp;provider. I tried installing this on my HTTPS&nbsp;server and ran into some security errors. Most browsers prevent making connections to insecure (HTTP) servers to&nbsp;a&nbsp;HTTPS&nbsp;server.</p>
<p>There are some ways to deal with that, however I’ve decided to take another road. Incase you decide to dive in here are some terms linked to some interesting articles:</p>
<ul>
<li><a href="https://www.html5rocks.com/en/tutorials/cors/">Cross-Origin Resource Sharing (CORS)</a></li>
<li><a href="https://en.wikipedia.org/wiki/JSONP">JSONP</a></li>
<li><a href="http://www.martinstoeckli.ch/php/php.html">X-Frame-Options and Content-Security-Policy</a></li>
</ul>
<p>In the shared Particle Photon code I&#8217;ve implemented a version that redirects you to my <a href="http://dev.kasperkamperman.com/localremote/" rel="nofollow">hosted remote control application</a> and a version that seems to run on the Photon (like the <a href="https://www.hackster.io/wgbartley/iot-device-management-with-mdns-and-webduino-93982a">iFrame idea of Garret Bartley</a>) by storing html code in a JavaScript.&nbsp;</p>
<p>You can try the methods below out directly by installing <a href="https://go.particle.io/shared_apps/59307698806fa632ca000e52">this Particle build project</a> on your Particle Photon.&nbsp;</p>
<h3>Redirect to online application</h3>
<p>The first is that if you surf to <a href="http://myphoton.local/">myphoton.local</a>, the Photon will redirect you to an index.html file on my web server and passes both the ip-address and the mDNS address. From that website we call then the address myphoton.local/sethsb/ and post a comma separated string. The server returns a JSON formatted string. Check the&nbsp;<a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/">Particle Photon Cloud Remote Control article</a>&nbsp;in which I explain my design choices.&nbsp;</p>
<p>It&#8217;s important to know that I redirect the user to my insecure (http) part of the website. Because doing a call from my secure (HTTPS) part to the Particle Photon server won&#8217;t work, because&nbsp;the server only supports&nbsp;http. You can try it yourself by opening <a href="https://dev.kasperkamperman.com/localremote/">the control application over HTTPS</a> (check the console for the error messages).&nbsp;</p>
<p>Of course the user will see that they get redirected to another page (<a href="https://www.hackster.io/wgbartley/iot-device-management-with-mdns-and-webduino-93982a">the iFrame version of Garret</a> made it appear that everything was running on the Photon), which might be something you won&#8217;t like. However there are benefits. You can easily bookmark the page and it will be reachable even if your Particle device is offline. In that way you can give a friendly error message (like I did by opening &#8220;Is your Particle device online?&#8221; &#8216;snack bar&#8217; dialog).</p>
<h3><strong>Serve a webpage on the Photon</strong></h3>
<p>I also wanted to explore the idea of serving the webpage from the Photon. In that way I liked the idea of <a href="https://www.hackster.io/wgbartley/iot-device-management-with-mdns-and-webduino-93982ahttps://www.hackster.io/wgbartley/iot-device-management-with-mdns-and-webduino-93982a">Garret</a> that used the iFrame to load the HTML, CSS and JavaScript externally, so you would save memory on the Photon.</p>
<p>I did some research and importing HTML&nbsp;is much more difficult than importing JavaScript (<a href="https://developers.google.com/speed/libraries/">like including a&nbsp;library&nbsp;hosted on the Google CDN</a>) or images from an external host. A solution might be using a technique like&nbsp;<a href="https://www.webcomponents.org/introduction">webcomponents</a> which at the time of writing are only supported in Google Chrome, although there is a <a href="https://www.webcomponents.org/polyfills/">polyfill</a> (some script that works around this on older browsers) available.</p>
<p>I decided to try what <a href="https://www.html5rocks.com/en/tutorials/webcomponents/imports/">Eric Bidelman would file under&nbsp;Crazy Hacks</a> to import HTML. I&#8217;ve stored&nbsp;the HTML body&nbsp;code in a JavaScript file and use the <a href="https://www.w3schools.com/jsref/prop_html_innerhtml.asp">innerhtml property</a>&nbsp;to place that content&nbsp;in the HTML body. The code below is the index page served by the Photon, you see that all the scripts, and styling are&nbsp;hosted externally. Between the HTML body-tags there is no code.</p>
<pre class="lang:arduino decode:true ">const unsigned char index_html[] =
"<!--<span class="hiddenSpellError" pre="" data-mce-bogus="1"-->DOCTYPE html&gt; "
"Particle Photon RGB LOCAL Remote<script src="\&quot;https://storage.googleapis.com/code.getmdl.io/1.3.0/material.min.js\&quot;"></script>" "" "" "" "<script src="\&quot;https://dev.kasperkamperman.com/localremote/js/rangetouch.js\&quot;"></script>" "<script src="\&quot;https://dev.kasperkamperman.com/localremote/js/index_content.js\&quot;"></script>" "<script src="\&quot;https://dev.kasperkamperman.com/localremote/js/index.js\&quot;"></script>" " ";</pre>
<p>The HTML body is stored in the index_content.js file. To make this work I&#8217;ve <a href="http://addslashes.onlinephpfunctions.com">added slashes</a> and I&#8217;ve <a href="http://www.textfixer.com/html/compress-html-compression.php">&#8220;compressed&#8221;</a>&nbsp;it. So I store&nbsp;it in a JavaScript file (<a href="https://dev.kasperkamperman.com/localremote/js/index_content.js">check the whole file here</a>).</p>
<p>That&#8217;s it. The HTML&nbsp;and CSS for the remote control app is exactly the same used for&nbsp;the <a href="https://www.kasperkamperman.com/blog/particle-photon-rgb-remote-cloud/">Photon Cloud remote code</a>. The index.js file is mainly different in the part where we communicate with the device.</p>
<p><a class="fasc-button fasc-size-large fasc-type-flat ico-fa fasc-ico-before fa-github-square" style="background-color: #000000; color: #ffffff;" href="https://github.com/kasperkamperman/Particle-RGB-RemoteControl">Check the code and Quickstart on GitHub</a></p>
<p>The post <a href="https://www.kasperkamperman.com/blog/arduino/particle-photon-rgb-remote-local/">Particle Photon RGB Local Remote Control</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Research Ambient Visuals</title>
		<link>https://www.kasperkamperman.com/projects/research-ambient-visuals/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Fri, 21 Apr 2017 11:21:16 +0000</pubDate>
				<category><![CDATA[Projects]]></category>
		<guid isPermaLink="false">https://www.kasperkamperman.com/?p=2031</guid>

					<description><![CDATA[<p>The relaxing effect of a dynamically changing ambient luminaire. </p>
<p>The post <a href="https://www.kasperkamperman.com/projects/research-ambient-visuals/">Research Ambient Visuals</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>During my study Master in Media Innovation, I&#8217;ve worked on an ambient light concept. I created the LumiFlower, a prototype luminaire designed to relax people. I&#8217;ve conducted a research where I compared the dynamic wall luminaire with a static poster on the wall. About 15 people where tested in each group. During this research strong evidence was found that the designed luminaire was more relaxing than a static poster on the wall..</p>
<p><a href="https://figshare.com/articles/Master_Thesis_Media_Innovation_-_Static_Art_versus_Ambient_Visuals/4879919">Master Thesis Media Innovation &#8211; Static Art versus Ambient Visuals</a></p>
<h5>Get in contact</h5>
<p>I&#8217;m currently working on new product concepts to implement this research. Please reach out if you are a product designer and what to implement this research and technology in one of your products. Possible markets: spa, wellness, waiting rooms and healthcare.</p>
<p><a href="https://cdn.kasperkamperman.com/media//projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower.jpg"><img loading="lazy" decoding="async" class="alignnone wp-image-2032 size-medium" src="https://cdn.kasperkamperman.com/media//projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower-480x271.jpg" alt="" width="480" height="271" srcset="https://kasperkamperman.com/media/projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower-480x271.jpg 480w, https://kasperkamperman.com/media/projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower-400x225.jpg 400w, https://kasperkamperman.com/media/projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower-768x433.jpg 768w, https://kasperkamperman.com/media/projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower-800x451.jpg 800w, https://kasperkamperman.com/media/projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower-1200x677.jpg 1200w, https://kasperkamperman.com/media/projects/research-ambient-visuals/Kasper-Kamperman-Ambient-Dynamic-Luminaire-Lumiflower.jpg 1509w" sizes="(max-width: 480px) 100vw, 480px" /></a></p>
<p>The post <a href="https://www.kasperkamperman.com/projects/research-ambient-visuals/">Research Ambient Visuals</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Extract</title>
		<link>https://www.kasperkamperman.com/projects/extract/</link>
		
		<dc:creator><![CDATA[Kasper Kamperman]]></dc:creator>
		<pubDate>Mon, 17 Sep 2018 08:46:26 +0000</pubDate>
				<category><![CDATA[Projects]]></category>
		<category><![CDATA[audio visualisation]]></category>
		<category><![CDATA[experience]]></category>
		<category><![CDATA[extract]]></category>
		<category><![CDATA[featured]]></category>
		<category><![CDATA[furniture design]]></category>
		<category><![CDATA[linkedin]]></category>
		<category><![CDATA[lounge]]></category>
		<category><![CDATA[organic forms]]></category>
		<category><![CDATA[show-control]]></category>
		<category><![CDATA[sync]]></category>
		<guid isPermaLink="false">http://127.0.0.1/?p=3</guid>

					<description><![CDATA[<p>An interactive audiovisual light experience focused on synchronised audio, visuals and light. </p>
<p>The post <a href="https://www.kasperkamperman.com/projects/extract/">Extract</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><iframe loading="lazy" title="Extract Project" src="https://player.vimeo.com/video/1909014?dnt=1&amp;app_id=122963" width="640" height="368" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe></p>
<p>The Extract Project was started as a graduation project by Kasper Kamperman and Sebastiaan Elstgeest. The name Extract refers to &#8220;extracting&#8221; music and visual content to allow translation of this content to other senses.</p>
<p>The primary goal of the project was to create an experience which starts with the integration of music visuals, light and space and ends with delivering the proof that combined media have an enhancing effect upon each other. Besides that, the visitors can within certain limits influence the experience. An important element is the lounging furniture object. The audience public can experience this special type of furniture, while at the same time the furniture is an integrated part of the experience itself with its multiple synchronized light effects coming from the heart of the furniture piece.</p>
<p>To realize the project we approached students of different educational institutes in Enschede ( University of Twente, AKI, Saxion, conservatory ) and created a multidisciplinary team ( musicians, technicians, product designers, programmers and more ).</p>
<p><a href="https://www.utoday.nl/news/51283/eerste_prijs_extractproject">Extract wins Europrix (article in Dutch)</a></p>
<p>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-01.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-02.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-03.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-04.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-05.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-06.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-07.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08.jpg'><img loading="lazy" decoding="async" width="480" height="361" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08-480x361.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08-480x361.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08-800x602.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08-1200x903.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-08.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09.jpg'><img loading="lazy" decoding="async" width="480" height="362" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09-480x362.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09-480x362.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09-768x578.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09-800x603.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09-1200x904.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-09.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
<a href='https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10.jpg'><img loading="lazy" decoding="async" width="480" height="320" src="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10-480x320.jpg" class="attachment-medium size-medium" alt="" srcset="https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10-480x320.jpg 480w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10-768x511.jpg 768w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10-800x533.jpg 800w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10-1200x799.jpg 1200w, https://kasperkamperman.com/media/projects/extract/Kasper-Kamperman-Sebastiaan-Elstgeest-Extract-Music-Lounge-Experience-10.jpg 1280w" sizes="(max-width: 480px) 100vw, 480px" /></a>
</p>
<p>The post <a href="https://www.kasperkamperman.com/projects/extract/">Extract</a> appeared first on <a href="https://www.kasperkamperman.com">Kasper Kamperman</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
