• #Introduction

    Welcome to the my tiny wiki!

    This is a place where I keep some of the code snippets that I want to have stored somewhere. If you spot mistakes I'd be glad to here them.

    Patrick.

    #Overwrite a property in ts

    type Modify<T, R> = Omit<T, keyof R> & R
    

    e.g. overwrite children with a more specific type

    export type Stack = Modify<Group, { children: Element[] }>
    

    #Feature Folder Pattern in nuxt3

    import { readdirSync } from 'fs'
    import { defineNuxtModule, addComponentsDir, addImportsDir, createResolver } from '@nuxt/kit'
    
    const featureFolder = 'features'
    
    export default defineNuxtModule({
      setup() {
        const resolver = createResolver(import.meta.url)
    
        readdirSync(featureFolder).forEach((feature) => {
          addComponentsDir({
            path: resolver.resolve(`../${featureFolder}/${feature}/components`),
            prefix: feature,
          })
    
          addImportsDir(resolver.resolve(`../${featureFolder}/${feature}/composables`))
    
          // we don't want feature utils to be imported in the app
          // addImportsDir(resolver.resolve(`../${featureFolder}/${feature}/utils`))
        })
      },
    })
    

    #Basic oauth flow

    define('SSO_CLIENT_ID', '');
    define('SSO_CLIENT_SECRET', '');
    define('SSO_METADATA', 'https://PROVIDER.COM/.well-known/openid-configuration');
    define('SSO_REDIRECT_URI', URL_PUBLIC . 'sso/callback');
    
    public static function action_signin() {
      AuthUser::load();
      if (AuthUser::isLoggedIn()) {
        redirect('/admin');
        return;
      }
    
      $metadata = self::http(SSO_METADATA);
    
      $_SESSION['state'] = bin2hex(random_bytes(5));
      $_SESSION['code_verifier'] = bin2hex(random_bytes(50));
      $code_challenge = self::base64_urlencode(hash('sha256', $_SESSION['code_verifier'], true));
    
      $authorize_url = $metadata->authorization_endpoint.'?'.http_build_query([
        'response_type' => 'code',
        'client_id' => SSO_CLIENT_ID,
        'redirect_uri' => SSO_REDIRECT_URI,
        'state' => $_SESSION['state'],
        'scope' => 'openid email profile',
        'code_challenge' => $code_challenge,
        'code_challenge_method' => 'S256',
      ]);
    
      redirect($authorize_url);
    }
    
    public static function action_callback() {
      if(!isset($_SESSION['state']) || !isset($_GET['state']) || $_SESSION['state'] != $_GET['state']) {
        die('Authorization server returned an invalid state parameter');
      }
    
      if(isset($_GET['error'])) {
        die('Authorization server returned an error: '.htmlspecialchars($_GET['error']));
      }
    
      $metadata = self::http(SSO_METADATA);
    
      $response = self::http($metadata->token_endpoint, [
        'grant_type' => 'authorization_code',
        'code' => $_GET['code'],
        'redirect_uri' => SSO_REDIRECT_URI,
        'client_id' => SSO_CLIENT_ID,
        'client_secret' => SSO_CLIENT_SECRET,
        'code_verifier' => $_SESSION['code_verifier'],
      ]);
    
      if (isset($response->error)) {
        die('Authorization server returned an error: '.htmlspecialchars($response->error));
      }
    
      $userinfo = self::http($metadata->userinfo_endpoint, [
        'access_token' => $response->access_token,
      ]);
    
      if (!isset($userinfo->sub)) {
        die('Error fetching access token');
      }
    
      $login = self::createOrLogin($userinfo);
      if (!$login) {
        die('Authentication request was not successful');
      }
    
      redirect('/dashboard');
    }
    

    #Feather / Fade to transparency or color in three.js

    Fade to transparency from a point (center).

    export function modAppendFade(shader, background) {
      // assumes: https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshbasic.glsl.js
      shader.vertexShader = shader.vertexShader.replace(
        /* glsl */`void main() {`,
        /* glsl */`
          varying vec4 v_position;
          void main() {
            v_position = modelMatrix * vec4(position.xyz, 1.0);
        `
      )
      shader.fragmentShader = /* glsl */`
        varying vec4 v_position;
        ${shader.fragmentShader}
      `.replace(
        /* glsl */`#include <dithering_fragment>`,
        /* glsl */`
            #include <dithering_fragment>
    
            vec3 center = vec3(0.0, 0.0, 0.0);
            vec4 background = vec4(${background || '0.93, 0.93, 0.93, 1.0'});
            float diameter = 800.0;
            float falloff = 0.006;
    
            float vDistance = distance(v_position.xyz, center);
            float factor = clamp((vDistance - diameter) * falloff, 0.0, 1.0);
            gl_FragColor = mix(gl_FragColor, background, factor);
          `
      )
    }
    
    node.material.onBeforeCompile = shader => modAppendFade(shader)
    node.material.transparent = true
    node.material.needsUpdate = true
    

    #Easing functions

    See Easing Functions Cheat Sheet

    function easeInOutCubic(x: number): number {
      return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
    }
    
    function easeOutElastic(x: number): number {
      const c4 = (2 * Math.PI) / 3;
            return x === 0
            ? 0
            : x === 1
            ? 1
            : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * c4) + 1;
    }
    
    function easeInCirc(x: number): number {
      return 1 - Math.sqrt(1 - Math.pow(x, 2));
    }
    

    #Three.js - Raycasting for Object Click

    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();
    
    window.addEventListener('click', (event) => {
      mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
      mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(scene.children, true);
      if (intersects.length > 0) {
        console.log('Clicked:', intersects[0].object);
      }
    });
    

    #CSS Scroll Snap for Carousels

    .carousel {
      display: flex;
      overflow-x: auto;
      scroll-snap-type: x mandatory;
    }
    .carousel > * {
      scroll-snap-align: start;
      flex: 0 0 auto;
    }
    

    #Get Scrollbar Width (cross-browser)

    function getScrollbarWidth() {
      const div = document.createElement('div');
      div.style.visibility = 'hidden';
      div.style.overflow = 'scroll';
      div.style.msOverflowStyle = 'scrollbar'; // for Edge
      div.style.width = '50px';
      div.style.height = '50px';
      document.body.appendChild(div);
      const inner = document.createElement('div');
      div.appendChild(inner);
      const scrollbarWidth = div.offsetWidth - inner.offsetWidth;
      div.remove();
      return scrollbarWidth;
    }
    

    #Hide arrows and crosses in inputs with tailwind

    // tailwind 3
    // export default {
      // plugins: [
        // plugin(function ({ addUtilities }) {
    
          addUtilities({
            '.appearance-clean': {
              'appearance': 'none',
              '-moz-appearance': 'textfield',
              '&::-webkit-inner-spin-button': {
                '-webkit-appearance': 'none',
              },
              '&::-webkit-outer-spin-button': {
                '-webkit-appearance': 'none',
              },
            },
          })
    
        // }),
      // ]
    // }
    
    /* tailwind v4 */
    @layer utils {
      .appearance-clean {
        appearance: none;
        -webkit-appearance: none;
        &::-webkit-search-decoration,
        &::-webkit-search-cancel-button,
        &::-webkit-inner-spin-button,
        &::-webkit-outer-spin-button {
          -webkit-appearance: none;
        }
      }
    }
    
    <input type="number" class="appearance-clean" />
    

    #Font Loading

    font-display: auto;
    font-display: block;
    font-display: swap;
    font-display: fallback;
    font-display: optional;
    

    block-period: renders characters invisible
    swap-period: time to swap font-set for an other one

    auto
    The font display strategy is defined by the user agent.

    block
    Gives the font face a short block period and an infinite swap period.

    swap
    Gives the font face an extremely small block period and an infinite swap period.

    fallback
    Gives the font face an extremely small block period and a short swap period.

    optional
    Gives the font face an extremely small block period and no swap period.

    #iOS search input

    iOS Safari adds some ugly styling to search inputs. Fixable with:

    input {   
      appearance: none!important; 
    }
    

    important because Apple adds them with input[type=search] and therefor has a higher priority.

    #Loop continuously with swiper.js

    The swiper needs loop and speed.
    observer is necessary when the 0 index is wrong.

    <swiper
      :slides-per-view="'auto'"
      :loop="true"
      :speed="5000"
      :prevent-interaction-on-transition="true"
      :observer="true"
      :observe-parents="true"
      @swiper="onSwiper"
    >
      <!-- ... -->
    </swiper>
    

    Once swiper js is initialized it can be looped.

    onSwiper(swiper) {
      const loop = () => {
        swiper.slideTo(swiper.slides.length - swiper.loopedSlides * 2);
        swiper.once('transitionEnd', () => {
          swiper.slideTo(0, 0, false);
          setTimeout(loop, 0);
        });
      }
      loop();
    }
    

    #Copy Text

    Copy from an input

    textarea.select(); 
    document.execCommand('copy');
    

    Copy from a node

    const selection = window.getSelection();
    const range = document.createRange();
    range.selectNodeContents(ELEMENT);
    selection.removeAllRanges();
    selection.addRange(range);
    
    document.execCommand('copy');
    
    selection.removeAllRanges();
    

    or

    navigator.clipboard.writeText(CONTENT)
    

    #Get browser Theme

    const browserTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
    

    #px and pt to rem

    @function px($px) { @return round($px * 0.0625rem * 100) / 100; }
    @function pt($pt) { @return px($pt * 1.333); }
    

    #Add Authorization header to axios

    axios.interceptors.request.use(function (config) {
        const token = store.getState().session.token;
        config.headers.Authorization =  token;
        return config;
    });
    

    #Sleep Promise

    const sleep = (t: number) => new Promise(r => setTimeout(r, t));
    

    #Modulo in js

    const mod = (n, m) => ((n % m) + m) % m;
    

    as % (reminder operation) does not work as expected with negative numbers

    #Linear Map

    const map = (value, x1, y1, x2, y2) => ((value - x1) * (y2 - x2)) / (y1 - x1) + x2;
    

    #Basic Animation

    function animate(timing, draw, duration) {
      return new Promise((resolve) => {
        const start = performance.now();
        requestAnimationFrame(function _animate(time) {
          let timeFraction = (time - start) / duration;
          if (timeFraction > 1) timeFraction = 1;
    
          const progress = timing ? timing(timeFraction) : timeFraction;
          draw(progress);
    
          if (timeFraction < 1) {
            requestAnimationFrame(_animate);
          } else {
            resolve();
          }
        });
      });
    }
    

    #CSS Font import

    Basic

    @font-face {
      font-family: "Mechanical";
      src: url("mechanical.otf") format("opentype");
    }
    
    .ttf     format("truetype")
    .woff    format("woff")
    .woff2   format("woff2")
    .otf     format("opentype")
    

    Vue

    @font-face {
        font-family: Mechanical;
        src: url("~@/assets/fonts/Mechanical.otf") format("opentype");
    }
    

    #Run PHP in Docker

    Run PHP inside a Dev-Docker container

    $ docker run -it --mount src="$(pwd)",target=/var/www/html,type=bind -p 3000:80 --name phpdev --sysctl net.ipv4.ip_unprivileged_port_start=0 --rm php:7.2-apache /bin/bash -c 'a2enmod rewrite; apache2-foreground'
    

    Different user prevent the easy manipulation of files, it can be solved quite easily with

    $ git config core.fileMode false
    $ chmod -R 777 .