Home yo

THINK
digital systems · interfaces · experiences
SYSTEM / ACTIVE
(function() { const field = document.getElementById("thought-field"); if (!field) return; const words = [ "CODE", "LOGIC", "SYSTEM", "STRUCTURE", "FUNCTION", "INTERFACE", "UX", "UI", "FLOW", "DESIGN", "BUILD", "CREATE", "DEBUG", "DEPLOY", "RESPONSIVE", "PERFORMANCE", "ACCESSIBILITY", "SCALABLE", "SEMANTIC", "COMPONENT", "API", "DATABASE", "JAVASCRIPT", "HTML", "CSS", "REACT", "FRAMEWORK", "ARCHITECTURE", "PROTOTYPE", "ITERATE", "OPTIMIZE", "PRECISION", "DETAIL", "MOTION", "INTERACTION", "DIGITAL", "WEB", "PRODUCT", "EXPERIENCE", "IDEA", "MEMORY", "PROCESS", "SIGNAL", "INPUT", "OUTPUT", "NETWORK", "01", "02", "03", "07", "12", "24", "001", "101", "//", "::" ]; const important = [ "SYSTEM", "INTERFACE", "ARCHITECTURE", "LOGIC", "EXPERIENCE", "CREATE" ]; words.forEach(function(word) { const el = document.createElement("div"); el.className = "thought-word" + ( important.includes(word) ? " important" : "" ); el.textContent = word; el.style.left = Math.random() * 100 + "%"; el.style.top = Math.random() * 100 + "%"; el.style.setProperty( "--moveX", (Math.random() * 400 - 200) + "px" ); el.style.setProperty( "--moveY", (Math.random() * 300 - 150) + "px" ); el.style.setProperty( "--rotate", (Math.random() * 20 - 10) + "deg" ); el.style.animationDuration = (14 + Math.random() * 22) + "s"; el.style.animationDelay = (Math.random() * -30) + "s"; field.appendChild(el); }); })();

Projects yo

Prices yo

Time4h
Hourly rate750 DKK
ScaleMedium
Quantity1
ComplexityMedium
UrgencyStandard
Estimated price
12.000 DKK
Pricing curveDrag the points
Move the points to change the shape of the project.
(function(){ const root = document.getElementById("shiro-pricing"); if(!root || root.dataset.initialized) return; root.dataset.initialized = "true"; /* ----------------------------- DATA ----------------------------- */ const scaleNames = [ "Tiny", "Small", "Medium", "Large", "Enterprise" ]; const complexityNames = [ "Simple", "Light", "Medium", "Advanced", "Complex" ]; const urgencyNames = [ "Standard", "Priority", "Rush" ]; const scaleMultiplier = [ 0.7, 0.9, 1, 1.25, 1.6 ]; const complexityMultiplier = [ 0.8, 0.95, 1, 1.25, 1.5 ]; const urgencyMultiplier = [ 1, 1.2, 1.5 ]; const stages = [ "Start", "Scope", "Build", "Review", "Polish", "Delivery" ]; /* These values control the draggable curve. 1 = normal Higher = more effort Lower = less effort */ let points = [ 0.75, 1.05, 1.40, 1.10, 0.90, 0.65 ]; let dragging = null; /* ----------------------------- ELEMENTS ----------------------------- */ const timeInput = root.querySelector("#sp-time-input"); const rateInput = root.querySelector("#sp-rate-input"); const scaleInput = root.querySelector("#sp-scale-input"); const quantityInput = root.querySelector("#sp-quantity-input"); const complexityInput = root.querySelector("#sp-complexity-input"); const urgencyInput = root.querySelector("#sp-urgency-input"); const graph = root.querySelector("#sp-graph"); const grid = root.querySelector("#sp-grid"); const curve = root.querySelector("#sp-curve"); const area = root.querySelector("#sp-area"); const pointLayer = root.querySelector("#sp-points"); /* ----------------------------- FORMAT ----------------------------- */ function money(value){ return Math.round(value) .toLocaleString("da-DK") + " DKK"; } /* ----------------------------- BASE PRICE ----------------------------- */ function calculateBase(){ const time = Number(timeInput.value); const rate = Number(rateInput.value); const scale = Number(scaleInput.value); const quantity = Number(quantityInput.value); const complexity = Number(complexityInput.value); const urgency = Number(urgencyInput.value); return ( time * rate * quantity * scaleMultiplier[scale] * complexityMultiplier[complexity] * urgencyMultiplier[urgency] ); } /* ----------------------------- CURVE FACTOR ----------------------------- */ function curveFactor(){ /* Average the draggable points. This means the graph is not decorative: moving it actually affects the final price. */ const average = points.reduce((sum,value) => sum + value,0) / points.length; return average; } /* ----------------------------- UPDATE TEXT ----------------------------- */ function updateLabels(){ const time = Number(timeInput.value); const rate = Number(rateInput.value); const scale = Number(scaleInput.value); const quantity = Number(quantityInput.value); const complexity = Number(complexityInput.value); const urgency = Number(urgencyInput.value); root.querySelector("#sp-time").textContent = time + "h"; root.querySelector("#sp-rate").textContent = rate.toLocaleString("da-DK") + " DKK"; root.querySelector("#sp-scale").textContent = scaleNames[scale]; root.querySelector("#sp-quantity").textContent = quantity; root.querySelector("#sp-complexity").textContent = complexityNames[complexity]; root.querySelector("#sp-urgency").textContent = urgencyNames[urgency]; const finalPrice = calculateBase() * curveFactor(); root.querySelector("#sp-total").textContent = money(finalPrice); } /* ----------------------------- GRAPH COORDINATES ----------------------------- */ const W = 720; const H = 400; const left = 55; const right = 680; const top = 45; const bottom = 315; const graphWidth = right - left; const graphHeight = bottom - top; function pointPosition(index){ const x = left + (graphWidth / (points.length - 1)) * index; /* Convert point value into SVG Y. 0.25 = low 2.5 = high */ const min = 0.25; const max = 2.5; const normalized = (points[index] - min) / (max - min); const y = bottom - normalized * graphHeight; return { x:x, y:y }; } /* ----------------------------- DRAW GRID ----------------------------- */ function drawGrid(){ grid.innerHTML = ""; for(let i=0;i<=5;i++){ const x = left + (graphWidth / 5) * i; grid.insertAdjacentHTML( "beforeend", '' ); } for(let i=0;i<=4;i++){ const y = top + (graphHeight / 4) * i; grid.insertAdjacentHTML( "beforeend", '' ); } } /* ----------------------------- DRAW CURVE ----------------------------- */ function drawGraph(){ let path = ""; let areaPath = ""; points.forEach(function(value,index){ const p = pointPosition(index); if(index === 0){ path = "M " + p.x + " " + p.y; areaPath = "M " + p.x + " " + bottom + " L " + p.x + " " + p.y; }else{ /* Smooth-ish curve using quadratic segments. */ const previous = pointPosition(index - 1); const midX = (previous.x + p.x) / 2; path += " Q " + midX + " " + previous.y + " " + p.x + " " + p.y; areaPath += " Q " + midX + " " + previous.y + " " + p.x + " " + p.y; } }); const last = pointPosition(points.length - 1); areaPath += " L " + last.x + " " + bottom + " Z"; curve.setAttribute("d",path); area.setAttribute("d",areaPath); pointLayer.innerHTML = ""; points.forEach(function(value,index){ const p = pointPosition(index); /* Price percentage */ const percentage = Math.round(value * 100); const circle = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); circle.setAttribute( "class", "point" ); circle.setAttribute( "cx", p.x ); circle.setAttribute( "cy", p.y ); circle.setAttribute( "r", 8 ); circle.dataset.index = index; pointLayer.appendChild(circle); /* Stage label */ const label = document.createElementNS( "http://www.w3.org/2000/svg", "text" ); label.setAttribute( "class", "stage" ); label.setAttribute( "x", p.x ); label.setAttribute( "y", bottom + 30 ); label.textContent = stages[index]; pointLayer.appendChild(label); /* Value label */ const valueLabel = document.createElementNS( "http://www.w3.org/2000/svg", "text" ); valueLabel.setAttribute( "class", "graph-value" ); valueLabel.setAttribute( "x", p.x ); valueLabel.setAttribute( "y", p.y - 17 ); valueLabel.textContent = percentage + "%"; pointLayer.appendChild(valueLabel); }); } /* ----------------------------- DRAW EVERYTHING ----------------------------- */ function render(){ updateLabels(); drawGrid(); drawGraph(); } /* ----------------------------- SLIDERS ----------------------------- */ [ timeInput, rateInput, scaleInput, quantityInput, complexityInput, urgencyInput ].forEach(function(input){ input.addEventListener( "input", render ); }); /* ----------------------------- DRAGGING ----------------------------- */ function getSVGPoint(event){ const rect = graph.getBoundingClientRect(); return { x: (event.clientX - rect.left) / rect.width * W, y: (event.clientY - rect.top) / rect.height * H }; } graph.addEventListener( "pointerdown", function(event){ const target = event.target; if( !target.classList.contains("point") ){ return; } dragging = Number(target.dataset.index); target.classList.add( "dragging" ); try{ target.setPointerCapture( event.pointerId ); }catch(e){} event.preventDefault(); } ); graph.addEventListener( "pointermove", function(event){ if(dragging === null){ return; } const p = getSVGPoint(event); const normalized = (bottom - p.y) / graphHeight; const min = 0.25; const max = 2.5; let value = min + normalized * (max - min); value = Math.max( min, Math.min(max,value) ); /* Don't allow the user to drag horizontally because the stages stay fixed in order. */ points[dragging] = value; render(); event.preventDefault(); } ); function stopDragging(){ dragging = null; } graph.addEventListener( "pointerup", stopDragging ); graph.addEventListener( "pointercancel", stopDragging ); /* ----------------------------- INITIALISE ----------------------------- */ render(); })();
PackagePrice (DKK)Est. TimePagesBest ForPlatform
Basic Standard Professional Custom4,000–6,000 7,000–10,000 12,000–18,000 20,000–40,000+4–7h 8–15h 15–25h 25–50h+1–3 4–6 6–10+ CustomNew / very small businesses Small local businesses Established businesses Complex requirementsCarrd, Framer WordPress, Framer WordPress, Webflow Custom code

about me yo

PERSONAL TRANSMISSION
SIGNAL / ACTIVE
CLIENT
TRANSMISSION / 01
CLIENT
YOU
TRANSMISSION / 02
DEVELOPER
CLIENT
TRANSMISSION 01 / 10
(function() { const conversation = [ { speaker: "CLIENT", text: "So... who are you?" }, { speaker: "YOU", text: "I'm a web developer. I take ideas, turn them into systems, and then make those systems feel good to use." }, { speaker: "CLIENT", text: "What does that actually mean?" }, { speaker: "YOU", text: "It means I care about what happens after the design looks good. The interaction. The structure. The performance. The details." }, { speaker: "CLIENT", text: "So you're a designer?" }, { speaker: "YOU", text: "Part designer. Part developer. Mostly someone who likes making the two stop fighting." }, { speaker: "CLIENT", text: "Why shouldn't I just use a template?" }, { speaker: "YOU", text: "You can. Templates are great when the problem is simply getting online. Custom work matters when the website needs to feel like it belongs to you." }, { speaker: "CLIENT", text: "What do you think makes a good website?" }, { speaker: "YOU", text: "Clarity first. Then interaction. Then the details people don't consciously notice — spacing, motion, hierarchy, responsiveness." }, { speaker: "CLIENT", text: "What if I don't know exactly what I want?" }, { speaker: "YOU", text: "That's fine. You don't need to arrive with the answer. You just need to know what you're trying to accomplish." }, { speaker: "CLIENT", text: "Do you only build websites?" }, { speaker: "YOU", text: "A website is only the surface. Underneath it is a system — content, structure, interaction, logic and a reason for someone to stay." }, { speaker: "CLIENT", text: "What do you obsess over?" }, { speaker: "YOU", text: "The small things. The movement of an element. The timing of an interaction. A strange three pixels of spacing that nobody can explain but somehow feels wrong." }, { speaker: "CLIENT", text: "And what do you want people to feel?" }, { speaker: "YOU", text: "That someone thought about every part of the experience." }, { speaker: "CLIENT", text: "So... what's the mission?" }, { speaker: "YOU", text: "Build something useful. Make it memorable. Keep it simple where it should be simple — and interesting where it deserves to be." } ]; const speaker = document.getElementById("shiroSpeaker"); const message = document.getElementById("shiroMessage"); const counter = document.getElementById("shiroCounter"); const next = document.getElementById("shiroNext"); let current = 0; let typingTimer = null; function typeMessage(text) { clearInterval(typingTimer); message.innerHTML = ''; let index = 0; typingTimer = setInterval(function() { if (index >= text.length) { clearInterval(typingTimer); message.innerHTML = text + ''; return; } message.innerHTML = text.substring(0, index + 1) + ''; index++; }, 24); } function showMessage() { const item = conversation[current]; speaker.textContent = item.speaker; counter.textContent = "TRANSMISSION " + String(current + 1).padStart(2, "0") + " / " + String(conversation.length).padStart(2, "0"); typeMessage(item.text); } next.addEventListener("click", function() { current++; if (current >= conversation.length) { current = 0; } showMessage(); }); document.addEventListener("keydown", function(e) { if ( e.key === "Enter" || e.key === "ArrowRight" ) { current++; if (current >= conversation.length) { current = 0; } showMessage(); } }); showMessage(); })();