From 2ce81f364bd5e95d81cecc75d5e79c70e2ebc89b Mon Sep 17 00:00:00 2001 From: Stanko Date: Wed, 1 Nov 2023 20:59:04 +0100 Subject: [PATCH 1/4] Canvas renderer --- docs/assets/index-19458268.js | 1 + docs/assets/index-2e5df5a1.css | 1 + docs/assets/index-3314c610.js | 1 - docs/assets/index-955a1911.css | 1 - docs/index.html | 9 ++- src/css/pulsar.scss | 9 +++ src/index.html | 5 ++ src/ts/canvas-grid/canvas.ts | 100 +++++++++++++++++++++++++ src/ts/canvas-grid/circles.ts | 25 +++++++ src/ts/canvas-grid/constants.ts | 2 + src/ts/canvas-grid/hex.ts | 47 ++++++++++++ src/ts/canvas-grid/index.ts | 30 ++++++++ src/ts/{grid => canvas-grid}/svg.ts | 0 src/ts/canvas-grid/triangles.ts | 55 ++++++++++++++ src/ts/lib/pulsar.ts | 58 +++++++------- src/ts/lib/types.ts | 8 ++ src/ts/{grid => svg-grid}/circles.ts | 0 src/ts/{grid => svg-grid}/hex.ts | 0 src/ts/{grid => svg-grid}/index.ts | 0 src/ts/svg-grid/svg.ts | 56 ++++++++++++++ src/ts/{grid => svg-grid}/triangles.ts | 0 21 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 docs/assets/index-19458268.js create mode 100644 docs/assets/index-2e5df5a1.css delete mode 100644 docs/assets/index-3314c610.js delete mode 100644 docs/assets/index-955a1911.css create mode 100644 src/ts/canvas-grid/canvas.ts create mode 100644 src/ts/canvas-grid/circles.ts create mode 100644 src/ts/canvas-grid/constants.ts create mode 100644 src/ts/canvas-grid/hex.ts create mode 100644 src/ts/canvas-grid/index.ts rename src/ts/{grid => canvas-grid}/svg.ts (100%) create mode 100644 src/ts/canvas-grid/triangles.ts rename src/ts/{grid => svg-grid}/circles.ts (100%) rename src/ts/{grid => svg-grid}/hex.ts (100%) rename src/ts/{grid => svg-grid}/index.ts (100%) create mode 100644 src/ts/svg-grid/svg.ts rename src/ts/{grid => svg-grid}/triangles.ts (100%) diff --git a/docs/assets/index-19458268.js b/docs/assets/index-19458268.js new file mode 100644 index 0000000..45dd5f4 --- /dev/null +++ b/docs/assets/index-19458268.js @@ -0,0 +1 @@ +var Q=Object.defineProperty;var Z=(s,t,e)=>t in s?Q(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>(Z(s,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))a(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function e(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function a(i){if(i.ep)return;i.ep=!0;const r=e(i);fetch(i.href,r)}})();const W=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let $={};W.addEventListener("message",s=>{$[s.data.id](s.data),delete $[s.data.id]});async function X(s,t,e){return new Promise(a=>{const i=Math.random().toString(16)+Date.now().toString(16);$[i]=a,W.postMessage({id:i,grid:s,t,userCode:e})})}const T=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function D(s,t=1){const e=Math.sqrt(Math.pow(s.x*t,2)+Math.pow(s.y*t,2));return T[Math.floor(e)]||T[T.length-1]}const x=400,R=x*.075,tt=1.1,et=R*.85;function st(s,t){const e=[],a=.5*tt,i=2*a/Math.sqrt(3),r=a*2,l=i*1.5;for(let u=-t;u<=t;u+=1){const h=u*l,p=u%2!==0,f=p?a:0,L=p?1:0;for(let g=-s-L;g<=s;g+=1){const S=g*r+f,m=D({x:S,y:h});e.push({x:S,y:h,color:m,r:i*et})}}return e}const A=1.33,H=.8;function it(s,t){const e=[],i=1*Math.sqrt(3)/2,r=i/3*2*R,l=1/2,u=i;for(let h=-t;h<=t;h+=1){const p=h*u,f=h%2!==0,L=f?l:0,g=f?2:1,S=f?0:1;for(let m=-s-g;m<=s+S;m+=1){const k=m*l+L,G=m%2!==0,J=G?Math.PI:0,F=G?i/3:0,K=D({x:k,y:p-F},1/H);e.push({x:k*A,y:(p-F)*A,r:r*H*A,color:K,angleOffset:J})}}return e}const at=.88,ot=.5*at;function nt(s){const t=[];for(let e=-s;e<=s;e+=1)for(let a=-s;a<=s;a+=1){const i=D({x:a,y:e});t.push({x:a,y:e,r:ot*R,color:i})}return t}const rt={classic:()=>nt(6),hex:()=>st(5,6),triangular:()=>it(8,5)};class ct{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=rt[t]()}}let v=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!v).toString()),window.location.reload()};v&&document.body.classList.add("debug");const lt=["grid","animate","code"];function dt(s){return encodeURIComponent(btoa(s))}function ht(s){try{return atob(decodeURIComponent(s))}catch{return""}}function N(s){const t=new URLSearchParams(window.location.search);Object.entries(s).forEach(([i,r])=>{i==="code"?t.set(i,dt(r)):t.set(i,r)});for(const i of t.keys())lt.includes(i)||t.delete(i);const e=t.toString(),a=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",a)}const C={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ut{constructor(){o(this,"code",C.code);o(this,"grid",C.grid);o(this,"animate",C.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(i=>i(e)));const a=document.querySelector(`input[name=${t}][value=${e}]`);a.checked=!0,N({[t]:e})}updateCode(t){t!==this.code&&(this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t}))}}const n=new ut,I=document.querySelector(".canvas"),P=window.devicePixelRatio,c=I.getContext("2d");I.width=x*P;I.height=x*P;c.scale(P,P);const{PI:mt}=Math,U=Math.PI/6,Y=U*2,pt=Y*2,q=[0,1,2,3,4,5].map(s=>U+s*Y),O=[0,1,2].map(s=>mt+U+s*pt);function ft(s,t){const{x:e,y:a}=s;c.moveTo(e+t*Math.cos(q[0]),a+t*Math.sin(q[0])),q.forEach(i=>{c.lineTo(e+t*Math.cos(i),a+t*Math.sin(i))})}function gt(s,t,e){const{x:a,y:i}=s;c.moveTo(a+t*Math.cos(O[0]+e),i+t*Math.sin(O[0]+e)),O.forEach(r=>{c.lineTo(a+t*Math.cos(r+e),i+t*Math.sin(r+e))})}function yt(s,t){const{x:e,y:a}=s;c.arc(e,a,t,0,2*Math.PI)}const wt={classic:yt,hex:ft,triangular:gt};function xt(s,t){c.clearRect(0,0,x,x);const e=n.animate==="scale"||n.animate==="both",a=n.animate==="opacity"||n.animate==="both";s.forEach((i,r)=>{const{color:l,x:u,y:h,r:p,angleOffset:f}=i,L=t[r],g=e?L:1,S=a?L:1,m=wt[n.grid];c.globalAlpha=S,c.fillStyle=l,c.beginPath(),m({x:u*R+x*.5,y:x*.5-h*R},p*g,f||0),c.fill()})}const Lt=document.querySelector(".pulsar"),St=document.querySelector(".play-pause"),Et=[...document.querySelectorAll(".toggle-ui")];Et.forEach(s=>{s.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const vt=document.querySelector("input[name=autoplay]");class Rt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await X(this.grid.points,this.timeSinceLastRestart,n.code);if(v&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}xt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ct(n.grid),this.isPlaying=vt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),St.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Lt.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"}],_=V[Math.floor(Math.random()*V.length)],bt=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Mt=300,Pt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=y.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const a of bt)if(t.includes(a)){this.showError(`Let's play nice, no usage of "${a}" allowed.`);return}const e=await X(At,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});y.addEventListener("input",this.validate),y.addEventListener("change",this.validate),this.updateFromURL(),y.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){y.value=t,this.highlightCode()}update(t){y.value=t,this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",a=ht(e)||_.code;a.length>Mt?this.update("Code in the URL is too long."):this.update(a)}highlightCode(){let t=y.value;for(const[e,a]of Object.entries(Pt))t=t.replace(a,`$1`);Tt.innerHTML=t}showError(t){n.error=t,j.innerHTML=t}hideError(){n.error="",j.innerHTML=""}}class B{constructor(t,e,a){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=a,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(i=>{i.addEventListener("change",()=>{n.updateRadio(this.name,i.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const b=document.querySelector(".tutorial"),z=document.querySelector(".intro-text");let w=null;const d=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:z.innerText,code:""}];class qt{constructor(t){d[d.length-1].code=n.code,d[d.length-1].grid=n.grid,d[d.length-1].animate=n.animate,b.addEventListener("click",()=>{w===null?w=0:w=(w+1)%d.length,w===d.length-2?b.innerHTML="Finish":w===d.length-1?b.innerHTML="Restart":b.innerHTML="Next";const e=d[w];z.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const E=document.querySelector(".share");let M=0;E.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(M),E.classList.add("share--success"),M=setTimeout(()=>{E.classList.remove("share--success")},3e3))}catch(s){if(s.name==="AbortError")return;clearTimeout(M),E.classList.add("share--error"),M=setTimeout(()=>{E.classList.remove("share--error")},3e3)}});new B("grid",["classic","hex","triangular"],_.grid||"");new B("animate",["scale","opacity","both"],_.animate||"");new Ct;const Ot=new Rt;new qt(Ot); diff --git a/docs/assets/index-2e5df5a1.css b/docs/assets/index-2e5df5a1.css new file mode 100644 index 0000000..57dd579 --- /dev/null +++ b/docs/assets/index-2e5df5a1.css @@ -0,0 +1 @@ +:root{line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;--bg-transparent: rgba(20, 34, 48, .85);--bg: rgb(20, 34, 48);--fg: #eee;--red: #ff2c55;--orange: #ff9500;--yellow: #ffcc02;--green: #35c759;--light-blue: #5bc7fa;--blue: #007aff;--purple: #5856d7;--mystic-magenta: #af52de;--red-contrast: hsl(348, 100%, 65%);--blue-contrast: hsl(211, 100%, 60%);--purple-contrast: hsl(241, 62%, 70%);--yellow-contrast: hsl(48, 100%, 60%);--text-opacity: .7}*{box-sizing:border-box;padding:0;margin:0;outline:none}*:focus-visible{outline:2px solid var(--purple-contrast)}svg{display:block}body{background-color:var(--bg);color:var(--fg);font-size:18px;font-family:Avenir,Montserrat,Corbel,URW Gothic,source-sans-pro,sans-serif}h1{font-weight:400;font-size:40px;line-height:1;letter-spacing:-.03em;color:var(--light-blue);font-family:ui-rounded,Hiragino Maru Gothic ProN,Quicksand,Comfortaa,Manjari,Arial Rounded MT,Arial Rounded MT Bold,Calibri,source-sans-pro,sans-serif;display:flex}@media (min-width: 600px){h1{font-size:48px}}h1 span:nth-child(1){color:var(--orange)}h1 span:nth-child(2){color:var(--yellow)}h1 span:nth-child(3){color:var(--green)}h1 span:nth-child(4){color:var(--light-blue)}h1 span:nth-child(5){color:var(--blue)}h1 span:nth-child(6){color:var(--purple)}a,a:visited{color:#dceaf7;text-decoration:none;outline:none}button{cursor:pointer}button,input{font:inherit}pre{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace}.pulsar{display:flex;flex-direction:column;min-height:100vh;min-height:100svh;max-width:1200px;margin:0 auto}@media (min-width: 1000px){.pulsar{flex-direction:row}}.top{width:100%}.title{display:flex;align-items:flex-end;margin-bottom:10px}.tutorial{margin-inline:8px auto}.intro-text{opacity:var(--text-opacity)}.icon-button,.button{background-color:#ffffff0d;border:none;color:#fff;border-radius:100px;font-size:16px;font-weight:700}.icon-button:hover,.button:hover{background-color:#ffffff26}.icon-button:active,.button:active{transform:translateY(1px)}.icon-button:focus-visible,.button:focus-visible{outline:2px solid var(--purple-contrast)}.icon-button{padding:10px;color:#ffffffb3}.icon-button:hover{color:#fff}.button{display:inline-flex;padding:10px 21px 10px 18px}.button svg{width:18px;height:18px}.button--sm{padding:3px 12px}.button span{display:inline-flex;align-items:center;gap:4px}.button:hover svg,.button:focus-visible svg{color:var(--purple-contrast)}.is-playing .play-pause__play-label,.is-paused .play-pause__pause-label{display:none}.canvas{display:block;width:100%}@media (min-width: 1000px){.canvas{margin-top:-10%}}.animation-wrapper{padding:30px;position:sticky;top:0;margin:auto;max-width:400px;width:100%}.animation{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center}@media (min-width: 1000px){.animation{margin-top:-10%}}.pixel-wrapper{width:7.5%;height:7.5%;position:relative}.pixel-wrapper[data-variant=triangular]{width:10%;height:10%}.pixel-wrapper svg{position:absolute;overflow:visible;will-change:transform;backface-visibility:hidden;transform:translateZ(0);width:100%;height:100%}@media (min-width: 600px){.toggle-ui{display:none}}.toggle-ui__absolute{display:none;position:absolute;top:10px;right:10px;z-index:1}@media (min-width: 600px){.toggle-ui__absolute{display:block}}.toggle-ui__show-label,.ui-hidden .controls{display:none}.ui-hidden .animation{margin-top:-10%}.ui-hidden .toggle-ui__absolute,.ui-hidden .toggle-ui__show-label{display:block}.ui-hidden .toggle-ui__hide-label{display:none}.controls{background-color:var(--bg-transparent);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);z-index:1;padding:30px;max-width:600px;width:100%;margin-inline:auto;border-radius:20px 20px 0 0;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;row-gap:15px}@media (min-width: 600px){.controls{padding:40px;row-gap:20px}}@media (min-width: 1000px){.controls{border-radius:0;width:450px;margin:0;padding:0 40px 40px;background:none;-webkit-backdrop-filter:none;backdrop-filter:none}}@media (min-width: 1300px){.controls{border-radius:0;width:600px;padding:0 60px 60px}}.radio__wrapper{display:flex;flex-wrap:wrap;column-gap:15px}.radio__name{margin-right:auto;flex:100% 0 0;font-size:16px;margin-bottom:4px;display:block}.radio__input{opacity:0;position:fixed;top:-500vh;left:-500vw;pointer-events:none;width:0;height:0}.radio__label{display:flex;align-items:center;gap:6px;border-radius:100px;padding-inline:5px;margin-inline:-5px;font-weight:700;font-size:16px;cursor:pointer}.radio__label:hover{text-decoration:underline}.radio__label svg:nth-child(2){display:none}.radio__input:focus-visible+.radio__label{outline:2px solid currentColor;outline-offset:2px}.radio__input:checked+.radio__label svg:nth-child(1){display:none}.radio__input:checked+.radio__label svg:nth-child(2){display:block}.radio__input--classic:checked+.radio__label{color:var(--green)}.radio__input--hex:checked+.radio__label{color:var(--light-blue)}.radio__input--triangular:checked+.radio__label{color:var(--blue-contrast)}.radio__input--scale:checked+.radio__label{color:var(--purple-contrast)}.radio__input--opacity:checked+.radio__label{color:var(--mystic-magenta)}.radio__input--both:checked+.radio__label{color:var(--red-contrast)}.radio__input--autoplay:checked+.radio__label{color:var(--yellow-contrast)}.editor{position:relative;width:100%}.editor__error{position:absolute;background-color:var(--bg);top:0;left:0;color:var(--orange);font-size:16px}.editor__label{font-size:16px;margin-bottom:4px;display:block}.editor__prefix{font-size:13px;margin-bottom:4px;opacity:var(--text-opacity)}.editor__wrapper{font-weight:400;text-align:left;width:100%;min-height:48px;position:relative;border-radius:10px;background:rgba(255,255,255,.05)}@media (min-width: 600px){.editor__wrapper{min-height:57px}}.editor__pre,.editor__textarea{overflow:hidden;width:100%;height:100%;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-size:17px;padding:10px 12px;border:none;display:block;line-height:1.6;border-radius:10px}@media (min-width: 600px){.editor__pre,.editor__textarea{font-size:18px;padding:14px 16px}}.editor__textarea{position:absolute;top:0;color:transparent;background:transparent;caret-color:#fff;resize:none}.editor__textarea .editor__textarea:focus{outline:4px solid rgba(255,255,255,.1)}.debug .editor__textarea{color:var(--light-blue);position:relative}.editor__pre{white-space:pre-wrap;word-wrap:break-word;overflow:hidden}.editor__pre i{font-style:normal}i.quotes{color:#e2d872}i.declaration{color:#64d5ea}i.fn{color:#a7e22e}i.parenthesis{color:#fed703}i.squared{color:#5fe8e8}i.curly{color:orchid}i.number{color:#9f77e7}i.logic{color:#f92572}i.equals{color:#5fe8e8}i.comments{color:#789;font-style:italic}i.comments>*{color:inherit}.bottom{display:flex;gap:8px;flex-wrap:wrap}.share .share__success-label{display:none}.share .share__success-label svg{color:var(--green)}.share .share__error-label,.share.share--success .share__default-label{display:none}.share.share--success .share__success-label{display:inline-flex}.share.share--error .share__default-label{display:none}.share.share--error .share__error-label{display:inline-flex} diff --git a/docs/assets/index-3314c610.js b/docs/assets/index-3314c610.js deleted file mode 100644 index 1834c1e..0000000 --- a/docs/assets/index-3314c610.js +++ /dev/null @@ -1 +0,0 @@ -var et=Object.defineProperty;var st=(i,t,e)=>t in i?et(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var o=(i,t,e)=>(st(i,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))a(s);new MutationObserver(s=>{for(const n of s)if(n.type==="childList")for(const c of n.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&a(c)}).observe(document,{childList:!0,subtree:!0});function e(s){const n={};return s.integrity&&(n.integrity=s.integrity),s.referrerPolicy&&(n.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?n.credentials="include":s.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function a(s){if(s.ep)return;s.ep=!0;const n=e(s);fetch(s.href,n)}})();const B=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let U={};B.addEventListener("message",i=>{U[i.data.id](i.data),delete U[i.data.id]});async function Y(i,t,e){return new Promise(a=>{const s=Math.random().toString(16)+Date.now().toString(16);U[s]=a,B.postMessage({id:s,grid:i,t,userCode:e})})}function _(i){const t=document.createElementNS("http://www.w3.org/2000/svg","svg");return t.setAttribute("viewBox",`0 0 ${i.width} ${i.height}`),t.setAttribute("width",`${i.width}`),t.setAttribute("height",`${i.height}`),t.style.opacity="0",t}function X(i,t={}){const e=document.createElementNS("http://www.w3.org/2000/svg","polygon");e.setAttribute("points",i.map(({x:a,y:s})=>`${a.toFixed(2)},${s.toFixed(2)}`).join(" "));for(const a in t)e.setAttribute(a,t[a]);return e}function it(i,t,e={}){const a=document.createElementNS("http://www.w3.org/2000/svg","circle");a.setAttribute("cx",i.x.toFixed(2)),a.setAttribute("cy",i.y.toFixed(2)),a.setAttribute("r",t.toFixed(2));for(const s in e)a.setAttribute(s,e[s]);return a}const O=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function k(i,t=1){const e=Math.sqrt(Math.pow(i.x*t,2)+Math.pow(i.y*t,2));return O[Math.floor(e)]||O[O.length-1]}const at=1.1,v=100,V=v*.85;function ot(i,t){const e=[],a=.5*at,s=2*a/Math.sqrt(3),n=a*2,c=s*1.5,C=Math.PI/3,P=Math.PI/6;for(let m=-t;m<=t;m+=1){const h=m*c,g=m%2!==0,y=g?a:0,q=g?1:0;for(let x=-i-q;x<=i;x+=1){const w=x*n+y,f=[0,1,2,3,4,5].map(F=>{const L=F*C+P;return{x:v*.5+s*V*Math.cos(L),y:v*.5+s*V*Math.sin(L)}}),d=_({width:v,height:v}),A=X(f,{fill:k({x:w,y:h})});d.classList.add("hex"),d.appendChild(A),d.style.left=`${(w*100).toFixed(2)}%`,d.style.top=`${(-h*100).toFixed(2)}%`,e.push({x:w,y:h,$element:d})}}return e}const D=.8,E=100;function nt(i,t){const e=[],s=1*Math.sqrt(3)/2,n=s/3*2*E,c=1/2,C=s,P=Math.PI/6,m=P*4;for(let h=-t;h<=t;h+=1){const g=h*C,y=h%2!==0,q=y?c:0,x=y?2:1,w=y?0:1;for(let f=-i-x;f<=i+w;f+=1){const d=f*c+q,A=f%2!==0,F=A?Math.PI:0,L=A?s/3:0,K=[0,1,2].map(tt=>{const H=tt*m+P+F;return{x:.5*E+n*D*Math.cos(H),y:.5*E+n*D*Math.sin(H)}}),S=_({width:E,height:E}),Q=X(K,{fill:k({x:d,y:g+L},1/D)});S.classList.add("triangle"),S.appendChild(Q),S.style.left=`${(d*100).toFixed(2)}%`,S.style.top=`${((g+L)*-100).toFixed(2)}%`,e.push({x:d,y:g,$element:S})}}return e}const rt=.88,ct=.5*rt,b=100;function lt(i){const t=[];for(let e=-i;e<=i;e+=1)for(let a=-i;a<=i;a+=1){const s=_({width:b,height:b}),n=it({x:b*.5,y:b*.5},ct*b,{fill:k({x:a,y:e})});s.appendChild(n),s.classList.add("circle"),s.style.left=`${(a*100).toFixed(2)}%`,s.style.top=`${(-e*100).toFixed(2)}%`,t.push({x:a,y:e,$element:s})}return t}const dt={classic:()=>lt(6),hex:()=>ot(5,6),triangular:()=>nt(8,5)},N=document.querySelector(".pixel-wrapper");class ht{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);o(this,"$points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t;const e=dt[t]();this.points=e.map(a=>({x:a.x,y:a.y})),this.$points=e.map(a=>a.$element),N.replaceChildren(...this.$points),N.dataset.variant=t}}let R=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!R).toString()),window.location.reload()};R&&document.body.classList.add("debug");const ut=["grid","animate","code"];function pt(i){return encodeURIComponent(btoa(i))}function mt(i){try{return atob(decodeURIComponent(i))}catch{return""}}function j(i){const t=new URLSearchParams(window.location.search);Object.entries(i).forEach(([s,n])=>{s==="code"?t.set(s,pt(n)):t.set(s,n)});for(const s of t.keys())ut.includes(s)||t.delete(s);const e=t.toString(),a=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",a)}const I={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class gt{constructor(){o(this,"code",I.code);o(this,"grid",I.grid);o(this,"animate",I.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(s=>s(e)));const a=document.querySelector(`input[name=${t}][value=${e}]`);a.checked=!0,j({[t]:e})}updateCode(t){t!==this.code&&(this.code=t,this.handlers.code.forEach(e=>e(t)),j({code:t}))}}const r=new gt,ft=document.querySelector(".pulsar"),yt=document.querySelector(".play-pause"),xt=[...document.querySelectorAll(".toggle-ui")];xt.forEach(i=>{i.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const wt=document.querySelector("input[name=autoplay]");class Lt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),r.error!==""){this.pauseOnError();return}const t=await Y(this.grid.points,this.timeSinceLastRestart,r.code);if(R&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}t.data.forEach((e,a)=>{const s=this.grid.$points[a];if(!s)return;const n=100;let c=(e===0?1e3:(1-1/e)*n).toFixed(2);r.animate==="scale"?(s.style.transform=`perspective(${n}px) translateZ(${c}px)`,s.style.opacity=""):r.animate==="opacity"?(s.style.opacity=e.toFixed(2),s.style.transform=""):(s.style.transform=`perspective(${n}px) translateZ(${c}px)`,s.style.opacity=e.toFixed(2))}),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ht(r.grid),this.isPlaying=wt.checked,this.playOrDraw(),r.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),r.onChange("animate",this.playOrDraw),r.onChange("code",()=>{this.playOrDraw()}),yt.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),ft.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),R&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,R&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const W=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"}],G=W[Math.floor(Math.random()*W.length)],St=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Et=300,bt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=u.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const a of St)if(t.includes(a)){this.showError(`Let's play nice, no usage of "${a}" allowed.`);return}const e=await Y(vt,0,t);if(e.error){this.showError(e.error);return}this.hideError(),r.updateCode(t)});u.addEventListener("input",this.validate),u.addEventListener("change",this.validate),this.updateFromURL(),u.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),r.onChange("code",t=>this.updateValue(t))}updateValue(t){u.value=t,this.highlightCode()}update(t){u.value=t,this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",a=mt(e)||G.code;a.length>Et?this.update("Code in the URL is too long."):this.update(a)}highlightCode(){let t=u.value;for(const[e,a]of Object.entries(bt))t=t.replace(a,`$1`);$t.innerHTML=t}showError(t){r.error=t,Z.innerHTML=t}hideError(){r.error="",Z.innerHTML=""}}class z{constructor(t,e,a){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=a,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(s=>{s.addEventListener("change",()=>{r.updateRadio(this.name,s.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),r.updateRadio(this.name,e)}update(t){r.updateRadio(this.name,t)}}const M=document.querySelector(".tutorial"),J=document.querySelector(".intro-text");let p=null;const l=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:J.innerText,code:""}];class Pt{constructor(t){l[l.length-1].code=r.code,l[l.length-1].grid=r.grid,l[l.length-1].animate=r.animate,M.addEventListener("click",()=>{p===null?p=0:p=(p+1)%l.length,p===l.length-2?M.innerHTML="Finish":p===l.length-1?M.innerHTML="Restart":M.innerHTML="Next";const e=l[p];J.innerHTML=e.text,r.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&r.updateRadio("grid",e.grid),e.animate&&r.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const $=document.querySelector(".share");let T=0;$.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(T),$.classList.add("share--success"),T=setTimeout(()=>{$.classList.remove("share--success")},3e3))}catch(i){if(i.name==="AbortError")return;clearTimeout(T),$.classList.add("share--error"),T=setTimeout(()=>{$.classList.remove("share--error")},3e3)}});new z("grid",["classic","hex","triangular"],G.grid||"");new z("animate",["scale","opacity","both"],G.animate||"");new Rt;const At=new Lt;new Pt(At); diff --git a/docs/assets/index-955a1911.css b/docs/assets/index-955a1911.css deleted file mode 100644 index 08d12a8..0000000 --- a/docs/assets/index-955a1911.css +++ /dev/null @@ -1 +0,0 @@ -:root{line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;--bg-transparent: rgba(20, 34, 48, .85);--bg: rgb(20, 34, 48);--fg: #eee;--red: #ff2c55;--orange: #ff9500;--yellow: #ffcc02;--green: #35c759;--light-blue: #5bc7fa;--blue: #007aff;--purple: #5856d7;--mystic-magenta: #af52de;--red-contrast: hsl(348, 100%, 65%);--blue-contrast: hsl(211, 100%, 60%);--purple-contrast: hsl(241, 62%, 70%);--yellow-contrast: hsl(48, 100%, 60%);--text-opacity: .7}*{box-sizing:border-box;padding:0;margin:0;outline:none}*:focus-visible{outline:2px solid var(--purple-contrast)}svg{display:block}body{background-color:var(--bg);color:var(--fg);font-size:18px;font-family:Avenir,Montserrat,Corbel,URW Gothic,source-sans-pro,sans-serif}h1{font-weight:400;font-size:40px;line-height:1;letter-spacing:-.03em;color:var(--light-blue);font-family:ui-rounded,Hiragino Maru Gothic ProN,Quicksand,Comfortaa,Manjari,Arial Rounded MT,Arial Rounded MT Bold,Calibri,source-sans-pro,sans-serif;display:flex}@media (min-width: 600px){h1{font-size:48px}}h1 span:nth-child(1){color:var(--orange)}h1 span:nth-child(2){color:var(--yellow)}h1 span:nth-child(3){color:var(--green)}h1 span:nth-child(4){color:var(--light-blue)}h1 span:nth-child(5){color:var(--blue)}h1 span:nth-child(6){color:var(--purple)}a,a:visited{color:#dceaf7;text-decoration:none;outline:none}button{cursor:pointer}button,input{font:inherit}pre{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace}.pulsar{display:flex;flex-direction:column;min-height:100vh;min-height:100svh;max-width:1200px;margin:0 auto}@media (min-width: 1000px){.pulsar{flex-direction:row}}.top{width:100%}.title{display:flex;align-items:flex-end;margin-bottom:10px}.tutorial{margin-inline:8px auto}.intro-text{opacity:var(--text-opacity)}.icon-button,.button{background-color:#ffffff0d;border:none;color:#fff;border-radius:100px;font-size:16px;font-weight:700}.icon-button:hover,.button:hover{background-color:#ffffff26}.icon-button:active,.button:active{transform:translateY(1px)}.icon-button:focus-visible,.button:focus-visible{outline:2px solid var(--purple-contrast)}.icon-button{padding:10px;color:#ffffffb3}.icon-button:hover{color:#fff}.button{display:inline-flex;padding:10px 21px 10px 18px}.button svg{width:18px;height:18px}.button--sm{padding:3px 12px}.button span{display:inline-flex;align-items:center;gap:4px}.button:hover svg,.button:focus-visible svg{color:var(--purple-contrast)}.is-playing .play-pause__play-label,.is-paused .play-pause__pause-label{display:none}.animation-wrapper{padding:30px;position:sticky;top:0;margin:auto;max-width:400px;width:100%}.animation{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center}@media (min-width: 1000px){.animation{margin-top:-10%}}.pixel-wrapper{width:7.5%;height:7.5%;position:relative}.pixel-wrapper[data-variant=triangular]{width:10%;height:10%}.pixel-wrapper svg{position:absolute;overflow:visible;will-change:transform;backface-visibility:hidden;transform:translateZ(0);width:100%;height:100%}@media (min-width: 600px){.toggle-ui{display:none}}.toggle-ui__absolute{display:none;position:absolute;top:10px;right:10px;z-index:1}@media (min-width: 600px){.toggle-ui__absolute{display:block}}.toggle-ui__show-label,.ui-hidden .controls{display:none}.ui-hidden .animation{margin-top:-10%}.ui-hidden .toggle-ui__absolute,.ui-hidden .toggle-ui__show-label{display:block}.ui-hidden .toggle-ui__hide-label{display:none}.controls{background-color:var(--bg-transparent);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);z-index:1;padding:30px;max-width:600px;width:100%;margin-inline:auto;border-radius:20px 20px 0 0;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;row-gap:15px}@media (min-width: 600px){.controls{padding:40px;row-gap:20px}}@media (min-width: 1000px){.controls{border-radius:0;width:450px;margin:0;padding:0 40px 40px;background:none;-webkit-backdrop-filter:none;backdrop-filter:none}}@media (min-width: 1300px){.controls{border-radius:0;width:600px;padding:0 60px 60px}}.radio__wrapper{display:flex;flex-wrap:wrap;column-gap:15px}.radio__name{margin-right:auto;flex:100% 0 0;font-size:16px;margin-bottom:4px;display:block}.radio__input{opacity:0;position:fixed;top:-500vh;left:-500vw;pointer-events:none;width:0;height:0}.radio__label{display:flex;align-items:center;gap:6px;border-radius:100px;padding-inline:5px;margin-inline:-5px;font-weight:700;font-size:16px;cursor:pointer}.radio__label:hover{text-decoration:underline}.radio__label svg:nth-child(2){display:none}.radio__input:focus-visible+.radio__label{outline:2px solid currentColor;outline-offset:2px}.radio__input:checked+.radio__label svg:nth-child(1){display:none}.radio__input:checked+.radio__label svg:nth-child(2){display:block}.radio__input--classic:checked+.radio__label{color:var(--green)}.radio__input--hex:checked+.radio__label{color:var(--light-blue)}.radio__input--triangular:checked+.radio__label{color:var(--blue-contrast)}.radio__input--scale:checked+.radio__label{color:var(--purple-contrast)}.radio__input--opacity:checked+.radio__label{color:var(--mystic-magenta)}.radio__input--both:checked+.radio__label{color:var(--red-contrast)}.radio__input--autoplay:checked+.radio__label{color:var(--yellow-contrast)}.editor{position:relative;width:100%}.editor__error{position:absolute;background-color:var(--bg);top:0;left:0;color:var(--orange);font-size:16px}.editor__label{font-size:16px;margin-bottom:4px;display:block}.editor__prefix{font-size:13px;margin-bottom:4px;opacity:var(--text-opacity)}.editor__wrapper{font-weight:400;text-align:left;width:100%;min-height:48px;position:relative;border-radius:10px;background:rgba(255,255,255,.05)}@media (min-width: 600px){.editor__wrapper{min-height:57px}}.editor__pre,.editor__textarea{overflow:hidden;width:100%;height:100%;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-size:17px;padding:10px 12px;border:none;display:block;line-height:1.6;border-radius:10px}@media (min-width: 600px){.editor__pre,.editor__textarea{font-size:18px;padding:14px 16px}}.editor__textarea{position:absolute;top:0;color:transparent;background:transparent;caret-color:#fff;resize:none}.editor__textarea .editor__textarea:focus{outline:4px solid rgba(255,255,255,.1)}.debug .editor__textarea{color:var(--light-blue);position:relative}.editor__pre{white-space:pre-wrap;word-wrap:break-word;overflow:hidden}.editor__pre i{font-style:normal}i.quotes{color:#e2d872}i.declaration{color:#64d5ea}i.fn{color:#a7e22e}i.parenthesis{color:#fed703}i.squared{color:#5fe8e8}i.curly{color:orchid}i.number{color:#9f77e7}i.logic{color:#f92572}i.equals{color:#5fe8e8}i.comments{color:#789;font-style:italic}i.comments>*{color:inherit}.bottom{display:flex;gap:8px;flex-wrap:wrap}.share .share__success-label{display:none}.share .share__success-label svg{color:var(--green)}.share .share__error-label,.share.share--success .share__default-label{display:none}.share.share--success .share__success-label{display:inline-flex}.share.share--error .share__default-label{display:none}.share.share--error .share__error-label{display:inline-flex} diff --git a/docs/index.html b/docs/index.html index 36dc182..3c6dc3b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -15,16 +15,21 @@ - - + +
+ +
+
diff --git a/src/css/pulsar.scss b/src/css/pulsar.scss index 689280c..0f5ea40 100644 --- a/src/css/pulsar.scss +++ b/src/css/pulsar.scss @@ -242,6 +242,15 @@ pre { // ----- Animation ----- // +.canvas { + display: block; + width: 100%; + + @include lg { + margin-top: -10%; + } +} + $pixel-size: 7.5%; .animation-wrapper { diff --git a/src/index.html b/src/index.html index 2212952..3d595a3 100644 --- a/src/index.html +++ b/src/index.html @@ -19,10 +19,15 @@
+ +
+
diff --git a/src/ts/canvas-grid/canvas.ts b/src/ts/canvas-grid/canvas.ts new file mode 100644 index 0000000..ee3dbc4 --- /dev/null +++ b/src/ts/canvas-grid/canvas.ts @@ -0,0 +1,100 @@ +import '../../css/pulsar.scss'; + +import { state } from '../lib/state'; +import { GridItem, Point } from '../lib/types'; +import { CANVAS_SIZE, RADIUS } from './constants'; + +const $canvas = document.querySelector('.canvas') as HTMLCanvasElement; + +const pixelRatio = window.devicePixelRatio; +const ctx = $canvas.getContext('2d') as CanvasRenderingContext2D; + +$canvas.width = CANVAS_SIZE * pixelRatio; +$canvas.height = CANVAS_SIZE * pixelRatio; + +ctx.scale(pixelRatio, pixelRatio); + +const { PI } = Math; +const DEG_30 = Math.PI / 6; +const DEG_60 = DEG_30 * 2; +const DEG_120 = DEG_60 * 2; + +const HEX_ANGLES = [0, 1, 2, 3, 4, 5].map((index) => { + return DEG_30 + index * DEG_60; +}); + +const TRIANGLE_ANGLES = [0, 1, 2].map((index) => { + return PI + DEG_30 + index * DEG_120; +}); + +function drawHexagon(center: Point, side: number) { + const { x, y } = center; + + ctx.moveTo( + x + side * Math.cos(HEX_ANGLES[0]), + y + side * Math.sin(HEX_ANGLES[0]) + ); + + HEX_ANGLES.forEach((angle) => { + ctx.lineTo(x + side * Math.cos(angle), y + side * Math.sin(angle)); + }); +} + +function drawTriangle(center: Point, side: number, angleOffset: number) { + const { x, y } = center; + + ctx.moveTo( + x + side * Math.cos(TRIANGLE_ANGLES[0] + angleOffset), + y + side * Math.sin(TRIANGLE_ANGLES[0] + angleOffset) + ); + + TRIANGLE_ANGLES.forEach((angle) => { + ctx.lineTo( + x + side * Math.cos(angle + angleOffset), + y + side * Math.sin(angle + angleOffset) + ); + }); +} + +function drawCircle(center: Point, radius: number) { + const { x, y } = center; + + ctx.arc(x, y, radius, 0, 2 * Math.PI); +} + +const drawMethods = { + classic: drawCircle, + hex: drawHexagon, + triangular: drawTriangle, +}; + +export function drawGrid(grid: GridItem[], data: number[]) { + ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE); + + const isScale = state.animate === 'scale' || state.animate === 'both'; + const isOpacity = state.animate === 'opacity' || state.animate === 'both'; + + grid.forEach((point, index) => { + const { color, x, y, r, angleOffset } = point; + const pointScale = data[index]; + const scale = isScale ? pointScale : 1; + const opacity = isOpacity ? pointScale : 1; + const drawMethod = drawMethods[state.grid]; + + ctx.globalAlpha = opacity; + ctx.fillStyle = color; + + ctx.beginPath(); + + drawMethod( + { + x: x * RADIUS + CANVAS_SIZE * 0.5, + y: CANVAS_SIZE * 0.5 - y * RADIUS, + }, + r * scale, + angleOffset || 0 + ); + + ctx.fill(); + }); +} diff --git a/src/ts/canvas-grid/circles.ts b/src/ts/canvas-grid/circles.ts new file mode 100644 index 0000000..6302159 --- /dev/null +++ b/src/ts/canvas-grid/circles.ts @@ -0,0 +1,25 @@ +import { GridItem } from '../lib/types'; +import { getColor } from '../lib/colors'; +import { RADIUS } from './constants'; + +const CIRCLE_SCALE = 0.88; +const r = 0.5 * CIRCLE_SCALE; + +export function generateCirclesGrid(size: number): GridItem[] { + const points: GridItem[] = []; + + for (let y = -size; y <= size; y += 1) { + for (let x = -size; x <= size; x += 1) { + const color = getColor({ x, y }); + + points.push({ + x, + y, + r: r * RADIUS, + color, + }); + } + } + + return points; +} diff --git a/src/ts/canvas-grid/constants.ts b/src/ts/canvas-grid/constants.ts new file mode 100644 index 0000000..9efd18f --- /dev/null +++ b/src/ts/canvas-grid/constants.ts @@ -0,0 +1,2 @@ +export const CANVAS_SIZE = 400; +export const RADIUS = CANVAS_SIZE * 0.075; diff --git a/src/ts/canvas-grid/hex.ts b/src/ts/canvas-grid/hex.ts new file mode 100644 index 0000000..66ea62f --- /dev/null +++ b/src/ts/canvas-grid/hex.ts @@ -0,0 +1,47 @@ +import { GridItem } from '../lib/types'; +import { getColor } from '../lib/colors'; +import { RADIUS } from './constants'; + +const SCALE = 1.1; // Scale to fit the grid into the SVG bounds +const HEX_SCALE = RADIUS * 0.85; + +export function generateHexGrid( + columnsCount: number, + rowsCount: number +): GridItem[] { + const points: GridItem[] = []; + + const innerRadius = 0.5 * SCALE; // height of the each of the six triangles in hexagon + const outerRadius = (2 * innerRadius) / Math.sqrt(3); // side of the hexagon and each of the six triangles in it + + const horizontalStep = innerRadius * 2; + const verticalStep = outerRadius * 1.5; + + for (let rowIndex = -rowsCount; rowIndex <= rowsCount; rowIndex += 1) { + const y = rowIndex * verticalStep; + + const isOddRow = rowIndex % 2 !== 0; + const horizontalOffset = isOddRow ? innerRadius : 0; + + const startOffset = isOddRow ? 1 : 0; + + for ( + let columnIndex = -columnsCount - startOffset; + columnIndex <= columnsCount; + columnIndex += 1 + ) { + const x = columnIndex * horizontalStep + horizontalOffset; + + const color = getColor({ x, y }); + + points.push({ + x, + y, + color, + r: outerRadius * HEX_SCALE, + }); + } + } + + return points; +} diff --git a/src/ts/canvas-grid/index.ts b/src/ts/canvas-grid/index.ts new file mode 100644 index 0000000..fa98b8a --- /dev/null +++ b/src/ts/canvas-grid/index.ts @@ -0,0 +1,30 @@ +import { GridItem, GridType } from '../lib/types'; + +import { generateHexGrid } from './hex'; +import { generateTriangleGrid } from './triangles'; +import { generateCirclesGrid } from './circles'; + +const gridMap: Record GridItem[]> = { + classic: () => generateCirclesGrid(6), + hex: () => generateHexGrid(5, 6), + triangular: () => generateTriangleGrid(8, 5), +}; + +// const $grid = document.querySelector('.pixel-wrapper') as HTMLDivElement; + +export class Grid { + type: GridType = 'classic'; + + points: GridItem[] = []; + + constructor(type: GridType = 'classic') { + this.type = type; + this.update(type); + } + + update(type: GridType = 'classic') { + this.type = type; + this.points = gridMap[type](); + // $grid.dataset.variant = type; + } +} diff --git a/src/ts/grid/svg.ts b/src/ts/canvas-grid/svg.ts similarity index 100% rename from src/ts/grid/svg.ts rename to src/ts/canvas-grid/svg.ts diff --git a/src/ts/canvas-grid/triangles.ts b/src/ts/canvas-grid/triangles.ts new file mode 100644 index 0000000..00615a1 --- /dev/null +++ b/src/ts/canvas-grid/triangles.ts @@ -0,0 +1,55 @@ +import { GridItem } from '../lib/types'; +import { getColor } from '../lib/colors'; +import { RADIUS } from './constants'; + +const SCALE = 1.33; // Scale to fit the grid into the SVG bounds +const TRIANGLE_SCALE = 0.8; + +export function generateTriangleGrid( + columnsCount: number, + rowsCount: number +): GridItem[] { + const points: GridItem[] = []; + + const side = 1; + const h = (side * Math.sqrt(3)) / 2; + const r = (h / 3) * 2 * RADIUS; + + const horizontalStep = side / 2; + const verticalStep = h; + + for (let rowIndex = -rowsCount; rowIndex <= rowsCount; rowIndex += 1) { + const y = rowIndex * verticalStep; + + const isOddRow = rowIndex % 2 !== 0; + const horizontalOffset = isOddRow ? horizontalStep : 0; + + const startOffset = isOddRow ? 2 : 1; + const endOffset = isOddRow ? 0 : 1; + + for ( + let columnIndex = -columnsCount - startOffset; + columnIndex <= columnsCount + endOffset; + columnIndex += 1 + ) { + const x = columnIndex * horizontalStep + horizontalOffset; + + const isOddColumn = columnIndex % 2 !== 0; + const angleOffset = isOddColumn ? Math.PI : 0; + + const yLocal = isOddColumn ? h / 3 : 0; + + const color = getColor({ x, y: y - yLocal }, 1 / TRIANGLE_SCALE); + + points.push({ + x: x * SCALE, + y: (y - yLocal) * SCALE, + r: r * TRIANGLE_SCALE * SCALE, + color, + angleOffset, + }); + } + } + + return points; +} diff --git a/src/ts/lib/pulsar.ts b/src/ts/lib/pulsar.ts index a708221..7485703 100644 --- a/src/ts/lib/pulsar.ts +++ b/src/ts/lib/pulsar.ts @@ -1,10 +1,11 @@ import { calculateGrid } from './calculate'; -import { Grid } from '../grid'; +import { Grid } from '../canvas-grid'; import debug from './debug'; import { state } from './state'; import { GridType } from './types'; +import { drawGrid } from '../canvas-grid/canvas'; // DOM const $root = document.querySelector('.pulsar') as HTMLElement; @@ -166,32 +167,35 @@ export class Pulsar { return; } - response.data.forEach((value: number, index: number) => { - const $point = this.grid.$points[index]; - - // Not every grid type has the same number of points. - // Therefore when the grid is changed multiple times in a single requestAnimationFrame, - // we need to check if the point exists - if (!$point) { - return; - } - - const PERSPECTIVE = 100; - - // Avoiding division by zero - let z = (value === 0 ? 1000 : (1 - 1 / value) * PERSPECTIVE).toFixed(2); - - if (state.animate === 'scale') { - $point.style.transform = `perspective(${PERSPECTIVE}px) translateZ(${z}px)`; - $point.style.opacity = ''; - } else if (state.animate === 'opacity') { - $point.style.opacity = value.toFixed(2); - $point.style.transform = ''; - } else { - $point.style.transform = `perspective(${PERSPECTIVE}px) translateZ(${z}px)`; - $point.style.opacity = value.toFixed(2); - } - }); + drawGrid(this.grid.points, response.data); + + // SVG renderer + // response.data.forEach((value: number, index: number) => { + // const $point = this.grid.$points[index]; + + // // Not every grid type has the same number of points. + // // Therefore when the grid is changed multiple times in a single requestAnimationFrame, + // // we need to check if the point exists + // if (!$point) { + // return; + // } + + // const PERSPECTIVE = 100; + + // // Avoiding division by zero + // let z = (value === 0 ? 1000 : (1 - 1 / value) * PERSPECTIVE).toFixed(2); + + // if (state.animate === 'scale') { + // $point.style.transform = `perspective(${PERSPECTIVE}px) translateZ(${z}px)`; + // $point.style.opacity = ''; + // } else if (state.animate === 'opacity') { + // $point.style.opacity = value.toFixed(2); + // $point.style.transform = ''; + // } else { + // $point.style.transform = `perspective(${PERSPECTIVE}px) translateZ(${z}px)`; + // $point.style.opacity = value.toFixed(2); + // } + // }); this.updateRootClass(); diff --git a/src/ts/lib/types.ts b/src/ts/lib/types.ts index 05ab8d8..17fd54a 100644 --- a/src/ts/lib/types.ts +++ b/src/ts/lib/types.ts @@ -63,3 +63,11 @@ export type WorkerResponse = { }; export type StateChangeHandler = (value: string) => void; + +export type GridItem = { + x: number; + y: number; + color: string; + r: number; + angleOffset?: number; +}; diff --git a/src/ts/grid/circles.ts b/src/ts/svg-grid/circles.ts similarity index 100% rename from src/ts/grid/circles.ts rename to src/ts/svg-grid/circles.ts diff --git a/src/ts/grid/hex.ts b/src/ts/svg-grid/hex.ts similarity index 100% rename from src/ts/grid/hex.ts rename to src/ts/svg-grid/hex.ts diff --git a/src/ts/grid/index.ts b/src/ts/svg-grid/index.ts similarity index 100% rename from src/ts/grid/index.ts rename to src/ts/svg-grid/index.ts diff --git a/src/ts/svg-grid/svg.ts b/src/ts/svg-grid/svg.ts new file mode 100644 index 0000000..37a892e --- /dev/null +++ b/src/ts/svg-grid/svg.ts @@ -0,0 +1,56 @@ +import { Point, SVGOptions } from '../lib/types'; + +export function create(options: SVGOptions): SVGElement { + const $svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + + $svg.setAttribute('viewBox', `0 0 ${options.width} ${options.height}`); + $svg.setAttribute('width', `${options.width}`); + $svg.setAttribute('height', `${options.height}`); + + // Opacity will be set in animation loop + $svg.style.opacity = '0'; + + return $svg; +} + +export function polygon( + points: Point[], + props: Record = {} +): SVGPolygonElement { + const $polygon = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'polygon' + ); + + $polygon.setAttribute( + 'points', + points.map(({ x, y }) => `${x.toFixed(2)},${y.toFixed(2)}`).join(' ') + ); + + for (const key in props) { + $polygon.setAttribute(key, props[key]); + } + + return $polygon; +} + +export function circle( + center: Point, + r: number, + props: Record = {} +): SVGCircleElement { + const $circle = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'circle' + ); + + $circle.setAttribute('cx', center.x.toFixed(2)); + $circle.setAttribute('cy', center.y.toFixed(2)); + $circle.setAttribute('r', r.toFixed(2)); + + for (const key in props) { + $circle.setAttribute(key, props[key]); + } + + return $circle; +} diff --git a/src/ts/grid/triangles.ts b/src/ts/svg-grid/triangles.ts similarity index 100% rename from src/ts/grid/triangles.ts rename to src/ts/svg-grid/triangles.ts From de8901c553ea5944e762d8abbe735be0349ad612 Mon Sep 17 00:00:00 2001 From: Stanko Date: Wed, 1 Nov 2023 21:06:52 +0100 Subject: [PATCH 2/4] Bug fix --- docs/assets/index-19458268.js | 1 - docs/assets/index-4501dc42.js | 1 + docs/index.html | 2 +- src/ts/lib/state.ts | 10 +++------- 4 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 docs/assets/index-19458268.js create mode 100644 docs/assets/index-4501dc42.js diff --git a/docs/assets/index-19458268.js b/docs/assets/index-19458268.js deleted file mode 100644 index 45dd5f4..0000000 --- a/docs/assets/index-19458268.js +++ /dev/null @@ -1 +0,0 @@ -var Q=Object.defineProperty;var Z=(s,t,e)=>t in s?Q(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>(Z(s,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))a(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function e(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function a(i){if(i.ep)return;i.ep=!0;const r=e(i);fetch(i.href,r)}})();const W=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let $={};W.addEventListener("message",s=>{$[s.data.id](s.data),delete $[s.data.id]});async function X(s,t,e){return new Promise(a=>{const i=Math.random().toString(16)+Date.now().toString(16);$[i]=a,W.postMessage({id:i,grid:s,t,userCode:e})})}const T=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function D(s,t=1){const e=Math.sqrt(Math.pow(s.x*t,2)+Math.pow(s.y*t,2));return T[Math.floor(e)]||T[T.length-1]}const x=400,R=x*.075,tt=1.1,et=R*.85;function st(s,t){const e=[],a=.5*tt,i=2*a/Math.sqrt(3),r=a*2,l=i*1.5;for(let u=-t;u<=t;u+=1){const h=u*l,p=u%2!==0,f=p?a:0,L=p?1:0;for(let g=-s-L;g<=s;g+=1){const S=g*r+f,m=D({x:S,y:h});e.push({x:S,y:h,color:m,r:i*et})}}return e}const A=1.33,H=.8;function it(s,t){const e=[],i=1*Math.sqrt(3)/2,r=i/3*2*R,l=1/2,u=i;for(let h=-t;h<=t;h+=1){const p=h*u,f=h%2!==0,L=f?l:0,g=f?2:1,S=f?0:1;for(let m=-s-g;m<=s+S;m+=1){const k=m*l+L,G=m%2!==0,J=G?Math.PI:0,F=G?i/3:0,K=D({x:k,y:p-F},1/H);e.push({x:k*A,y:(p-F)*A,r:r*H*A,color:K,angleOffset:J})}}return e}const at=.88,ot=.5*at;function nt(s){const t=[];for(let e=-s;e<=s;e+=1)for(let a=-s;a<=s;a+=1){const i=D({x:a,y:e});t.push({x:a,y:e,r:ot*R,color:i})}return t}const rt={classic:()=>nt(6),hex:()=>st(5,6),triangular:()=>it(8,5)};class ct{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=rt[t]()}}let v=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!v).toString()),window.location.reload()};v&&document.body.classList.add("debug");const lt=["grid","animate","code"];function dt(s){return encodeURIComponent(btoa(s))}function ht(s){try{return atob(decodeURIComponent(s))}catch{return""}}function N(s){const t=new URLSearchParams(window.location.search);Object.entries(s).forEach(([i,r])=>{i==="code"?t.set(i,dt(r)):t.set(i,r)});for(const i of t.keys())lt.includes(i)||t.delete(i);const e=t.toString(),a=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",a)}const C={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ut{constructor(){o(this,"code",C.code);o(this,"grid",C.grid);o(this,"animate",C.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(i=>i(e)));const a=document.querySelector(`input[name=${t}][value=${e}]`);a.checked=!0,N({[t]:e})}updateCode(t){t!==this.code&&(this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t}))}}const n=new ut,I=document.querySelector(".canvas"),P=window.devicePixelRatio,c=I.getContext("2d");I.width=x*P;I.height=x*P;c.scale(P,P);const{PI:mt}=Math,U=Math.PI/6,Y=U*2,pt=Y*2,q=[0,1,2,3,4,5].map(s=>U+s*Y),O=[0,1,2].map(s=>mt+U+s*pt);function ft(s,t){const{x:e,y:a}=s;c.moveTo(e+t*Math.cos(q[0]),a+t*Math.sin(q[0])),q.forEach(i=>{c.lineTo(e+t*Math.cos(i),a+t*Math.sin(i))})}function gt(s,t,e){const{x:a,y:i}=s;c.moveTo(a+t*Math.cos(O[0]+e),i+t*Math.sin(O[0]+e)),O.forEach(r=>{c.lineTo(a+t*Math.cos(r+e),i+t*Math.sin(r+e))})}function yt(s,t){const{x:e,y:a}=s;c.arc(e,a,t,0,2*Math.PI)}const wt={classic:yt,hex:ft,triangular:gt};function xt(s,t){c.clearRect(0,0,x,x);const e=n.animate==="scale"||n.animate==="both",a=n.animate==="opacity"||n.animate==="both";s.forEach((i,r)=>{const{color:l,x:u,y:h,r:p,angleOffset:f}=i,L=t[r],g=e?L:1,S=a?L:1,m=wt[n.grid];c.globalAlpha=S,c.fillStyle=l,c.beginPath(),m({x:u*R+x*.5,y:x*.5-h*R},p*g,f||0),c.fill()})}const Lt=document.querySelector(".pulsar"),St=document.querySelector(".play-pause"),Et=[...document.querySelectorAll(".toggle-ui")];Et.forEach(s=>{s.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const vt=document.querySelector("input[name=autoplay]");class Rt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await X(this.grid.points,this.timeSinceLastRestart,n.code);if(v&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}xt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ct(n.grid),this.isPlaying=vt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),St.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Lt.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"}],_=V[Math.floor(Math.random()*V.length)],bt=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Mt=300,Pt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=y.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const a of bt)if(t.includes(a)){this.showError(`Let's play nice, no usage of "${a}" allowed.`);return}const e=await X(At,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});y.addEventListener("input",this.validate),y.addEventListener("change",this.validate),this.updateFromURL(),y.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){y.value=t,this.highlightCode()}update(t){y.value=t,this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",a=ht(e)||_.code;a.length>Mt?this.update("Code in the URL is too long."):this.update(a)}highlightCode(){let t=y.value;for(const[e,a]of Object.entries(Pt))t=t.replace(a,`$1`);Tt.innerHTML=t}showError(t){n.error=t,j.innerHTML=t}hideError(){n.error="",j.innerHTML=""}}class B{constructor(t,e,a){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=a,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(i=>{i.addEventListener("change",()=>{n.updateRadio(this.name,i.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const b=document.querySelector(".tutorial"),z=document.querySelector(".intro-text");let w=null;const d=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:z.innerText,code:""}];class qt{constructor(t){d[d.length-1].code=n.code,d[d.length-1].grid=n.grid,d[d.length-1].animate=n.animate,b.addEventListener("click",()=>{w===null?w=0:w=(w+1)%d.length,w===d.length-2?b.innerHTML="Finish":w===d.length-1?b.innerHTML="Restart":b.innerHTML="Next";const e=d[w];z.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const E=document.querySelector(".share");let M=0;E.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(M),E.classList.add("share--success"),M=setTimeout(()=>{E.classList.remove("share--success")},3e3))}catch(s){if(s.name==="AbortError")return;clearTimeout(M),E.classList.add("share--error"),M=setTimeout(()=>{E.classList.remove("share--error")},3e3)}});new B("grid",["classic","hex","triangular"],_.grid||"");new B("animate",["scale","opacity","both"],_.animate||"");new Ct;const Ot=new Rt;new qt(Ot); diff --git a/docs/assets/index-4501dc42.js b/docs/assets/index-4501dc42.js new file mode 100644 index 0000000..3ab626c --- /dev/null +++ b/docs/assets/index-4501dc42.js @@ -0,0 +1 @@ +var Q=Object.defineProperty;var Z=(s,t,e)=>t in s?Q(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>(Z(s,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const r of a)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function e(a){const r={};return a.integrity&&(r.integrity=a.integrity),a.referrerPolicy&&(r.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?r.credentials="include":a.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(a){if(a.ep)return;a.ep=!0;const r=e(a);fetch(a.href,r)}})();const W=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let $={};W.addEventListener("message",s=>{$[s.data.id](s.data),delete $[s.data.id]});async function X(s,t,e){return new Promise(i=>{const a=Math.random().toString(16)+Date.now().toString(16);$[a]=i,W.postMessage({id:a,grid:s,t,userCode:e})})}const T=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function D(s,t=1){const e=Math.sqrt(Math.pow(s.x*t,2)+Math.pow(s.y*t,2));return T[Math.floor(e)]||T[T.length-1]}const x=400,R=x*.075,tt=1.1,et=R*.85;function st(s,t){const e=[],i=.5*tt,a=2*i/Math.sqrt(3),r=i*2,l=a*1.5;for(let u=-t;u<=t;u+=1){const h=u*l,p=u%2!==0,f=p?i:0,L=p?1:0;for(let g=-s-L;g<=s;g+=1){const S=g*r+f,m=D({x:S,y:h});e.push({x:S,y:h,color:m,r:a*et})}}return e}const A=1.33,H=.8;function at(s,t){const e=[],a=1*Math.sqrt(3)/2,r=a/3*2*R,l=1/2,u=a;for(let h=-t;h<=t;h+=1){const p=h*u,f=h%2!==0,L=f?l:0,g=f?2:1,S=f?0:1;for(let m=-s-g;m<=s+S;m+=1){const k=m*l+L,G=m%2!==0,J=G?Math.PI:0,F=G?a/3:0,K=D({x:k,y:p-F},1/H);e.push({x:k*A,y:(p-F)*A,r:r*H*A,color:K,angleOffset:J})}}return e}const it=.88,ot=.5*it;function nt(s){const t=[];for(let e=-s;e<=s;e+=1)for(let i=-s;i<=s;i+=1){const a=D({x:i,y:e});t.push({x:i,y:e,r:ot*R,color:a})}return t}const rt={classic:()=>nt(6),hex:()=>st(5,6),triangular:()=>at(8,5)};class ct{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=rt[t]()}}let v=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!v).toString()),window.location.reload()};v&&document.body.classList.add("debug");const lt=["grid","animate","code"];function dt(s){return encodeURIComponent(btoa(s))}function ht(s){try{return atob(decodeURIComponent(s))}catch{return""}}function N(s){const t=new URLSearchParams(window.location.search);Object.entries(s).forEach(([a,r])=>{a==="code"?t.set(a,dt(r)):t.set(a,r)});for(const a of t.keys())lt.includes(a)||t.delete(a);const e=t.toString(),i=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",i)}const C={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ut{constructor(){o(this,"code",C.code);o(this,"grid",C.grid);o(this,"animate",C.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(a=>a(e)));const i=document.querySelector(`input[name=${t}][value=${e}]`);i.checked=!0,N({[t]:e})}updateCode(t){this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t})}}const n=new ut,I=document.querySelector(".canvas"),P=window.devicePixelRatio,c=I.getContext("2d");I.width=x*P;I.height=x*P;c.scale(P,P);const{PI:mt}=Math,U=Math.PI/6,Y=U*2,pt=Y*2,q=[0,1,2,3,4,5].map(s=>U+s*Y),O=[0,1,2].map(s=>mt+U+s*pt);function ft(s,t){const{x:e,y:i}=s;c.moveTo(e+t*Math.cos(q[0]),i+t*Math.sin(q[0])),q.forEach(a=>{c.lineTo(e+t*Math.cos(a),i+t*Math.sin(a))})}function gt(s,t,e){const{x:i,y:a}=s;c.moveTo(i+t*Math.cos(O[0]+e),a+t*Math.sin(O[0]+e)),O.forEach(r=>{c.lineTo(i+t*Math.cos(r+e),a+t*Math.sin(r+e))})}function yt(s,t){const{x:e,y:i}=s;c.arc(e,i,t,0,2*Math.PI)}const wt={classic:yt,hex:ft,triangular:gt};function xt(s,t){c.clearRect(0,0,x,x);const e=n.animate==="scale"||n.animate==="both",i=n.animate==="opacity"||n.animate==="both";s.forEach((a,r)=>{const{color:l,x:u,y:h,r:p,angleOffset:f}=a,L=t[r],g=e?L:1,S=i?L:1,m=wt[n.grid];c.globalAlpha=S,c.fillStyle=l,c.beginPath(),m({x:u*R+x*.5,y:x*.5-h*R},p*g,f||0),c.fill()})}const Lt=document.querySelector(".pulsar"),St=document.querySelector(".play-pause"),Et=[...document.querySelectorAll(".toggle-ui")];Et.forEach(s=>{s.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const vt=document.querySelector("input[name=autoplay]");class Rt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await X(this.grid.points,this.timeSinceLastRestart,n.code);if(v&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}xt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ct(n.grid),this.isPlaying=vt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),St.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Lt.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"}],_=V[Math.floor(Math.random()*V.length)],bt=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Mt=300,Pt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=y.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const i of bt)if(t.includes(i)){this.showError(`Let's play nice, no usage of "${i}" allowed.`);return}const e=await X(At,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});y.addEventListener("input",this.validate),y.addEventListener("change",this.validate),this.updateFromURL(),y.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){y.value=t,this.highlightCode()}update(t){y.value=t,this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",i=ht(e)||_.code;i.length>Mt?this.update("Code in the URL is too long."):this.update(i)}highlightCode(){let t=y.value;for(const[e,i]of Object.entries(Pt))t=t.replace(i,`$1`);Tt.innerHTML=t}showError(t){n.error=t,j.innerHTML=t}hideError(){n.error="",j.innerHTML=""}}class B{constructor(t,e,i){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=i,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(a=>{a.addEventListener("change",()=>{n.updateRadio(this.name,a.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const b=document.querySelector(".tutorial"),z=document.querySelector(".intro-text");let w=null;const d=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:z.innerText,code:""}];class qt{constructor(t){d[d.length-1].code=n.code,d[d.length-1].grid=n.grid,d[d.length-1].animate=n.animate,b.addEventListener("click",()=>{w===null?w=0:w=(w+1)%d.length,w===d.length-2?b.innerHTML="Finish":w===d.length-1?b.innerHTML="Restart":b.innerHTML="Next";const e=d[w];z.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const E=document.querySelector(".share");let M=0;E.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(M),E.classList.add("share--success"),M=setTimeout(()=>{E.classList.remove("share--success")},3e3))}catch(s){if(s.name==="AbortError")return;clearTimeout(M),E.classList.add("share--error"),M=setTimeout(()=>{E.classList.remove("share--error")},3e3)}});new B("grid",["classic","hex","triangular"],_.grid||"");new B("animate",["scale","opacity","both"],_.animate||"");new Ct;const Ot=new Rt;new qt(Ot); diff --git a/docs/index.html b/docs/index.html index 3c6dc3b..62258ea 100644 --- a/docs/index.html +++ b/docs/index.html @@ -15,7 +15,7 @@ - + diff --git a/src/ts/lib/state.ts b/src/ts/lib/state.ts index 86d60e7..1eb9bbd 100644 --- a/src/ts/lib/state.ts +++ b/src/ts/lib/state.ts @@ -57,13 +57,9 @@ class State { } updateCode(code: string) { - if (code !== this.code) { - this.code = code; - - this.handlers['code'].forEach((fn) => fn(code)); - - updateURLQuery({ code }); - } + this.code = code; + this.handlers['code'].forEach((fn) => fn(code)); + updateURLQuery({ code }); } } From b19f493b9e13c4afbf6dda6bcc8e800e88666826 Mon Sep 17 00:00:00 2001 From: Stanko Date: Thu, 2 Nov 2023 09:55:57 +0100 Subject: [PATCH 3/4] New example --- docs/assets/{index-4501dc42.js => index-fc504c8f.js} | 2 +- docs/index.html | 2 +- src/ts/lib/debug.ts | 6 ++++++ src/ts/lib/editor.ts | 11 ++++++++--- src/ts/lib/examples.ts | 3 +++ 5 files changed, 19 insertions(+), 5 deletions(-) rename docs/assets/{index-4501dc42.js => index-fc504c8f.js} (50%) diff --git a/docs/assets/index-4501dc42.js b/docs/assets/index-fc504c8f.js similarity index 50% rename from docs/assets/index-4501dc42.js rename to docs/assets/index-fc504c8f.js index 3ab626c..6298cd7 100644 --- a/docs/assets/index-4501dc42.js +++ b/docs/assets/index-fc504c8f.js @@ -1 +1 @@ -var Q=Object.defineProperty;var Z=(s,t,e)=>t in s?Q(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>(Z(s,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const r of a)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function e(a){const r={};return a.integrity&&(r.integrity=a.integrity),a.referrerPolicy&&(r.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?r.credentials="include":a.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(a){if(a.ep)return;a.ep=!0;const r=e(a);fetch(a.href,r)}})();const W=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let $={};W.addEventListener("message",s=>{$[s.data.id](s.data),delete $[s.data.id]});async function X(s,t,e){return new Promise(i=>{const a=Math.random().toString(16)+Date.now().toString(16);$[a]=i,W.postMessage({id:a,grid:s,t,userCode:e})})}const T=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function D(s,t=1){const e=Math.sqrt(Math.pow(s.x*t,2)+Math.pow(s.y*t,2));return T[Math.floor(e)]||T[T.length-1]}const x=400,R=x*.075,tt=1.1,et=R*.85;function st(s,t){const e=[],i=.5*tt,a=2*i/Math.sqrt(3),r=i*2,l=a*1.5;for(let u=-t;u<=t;u+=1){const h=u*l,p=u%2!==0,f=p?i:0,L=p?1:0;for(let g=-s-L;g<=s;g+=1){const S=g*r+f,m=D({x:S,y:h});e.push({x:S,y:h,color:m,r:a*et})}}return e}const A=1.33,H=.8;function at(s,t){const e=[],a=1*Math.sqrt(3)/2,r=a/3*2*R,l=1/2,u=a;for(let h=-t;h<=t;h+=1){const p=h*u,f=h%2!==0,L=f?l:0,g=f?2:1,S=f?0:1;for(let m=-s-g;m<=s+S;m+=1){const k=m*l+L,G=m%2!==0,J=G?Math.PI:0,F=G?a/3:0,K=D({x:k,y:p-F},1/H);e.push({x:k*A,y:(p-F)*A,r:r*H*A,color:K,angleOffset:J})}}return e}const it=.88,ot=.5*it;function nt(s){const t=[];for(let e=-s;e<=s;e+=1)for(let i=-s;i<=s;i+=1){const a=D({x:i,y:e});t.push({x:i,y:e,r:ot*R,color:a})}return t}const rt={classic:()=>nt(6),hex:()=>st(5,6),triangular:()=>at(8,5)};class ct{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=rt[t]()}}let v=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!v).toString()),window.location.reload()};v&&document.body.classList.add("debug");const lt=["grid","animate","code"];function dt(s){return encodeURIComponent(btoa(s))}function ht(s){try{return atob(decodeURIComponent(s))}catch{return""}}function N(s){const t=new URLSearchParams(window.location.search);Object.entries(s).forEach(([a,r])=>{a==="code"?t.set(a,dt(r)):t.set(a,r)});for(const a of t.keys())lt.includes(a)||t.delete(a);const e=t.toString(),i=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",i)}const C={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ut{constructor(){o(this,"code",C.code);o(this,"grid",C.grid);o(this,"animate",C.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(a=>a(e)));const i=document.querySelector(`input[name=${t}][value=${e}]`);i.checked=!0,N({[t]:e})}updateCode(t){this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t})}}const n=new ut,I=document.querySelector(".canvas"),P=window.devicePixelRatio,c=I.getContext("2d");I.width=x*P;I.height=x*P;c.scale(P,P);const{PI:mt}=Math,U=Math.PI/6,Y=U*2,pt=Y*2,q=[0,1,2,3,4,5].map(s=>U+s*Y),O=[0,1,2].map(s=>mt+U+s*pt);function ft(s,t){const{x:e,y:i}=s;c.moveTo(e+t*Math.cos(q[0]),i+t*Math.sin(q[0])),q.forEach(a=>{c.lineTo(e+t*Math.cos(a),i+t*Math.sin(a))})}function gt(s,t,e){const{x:i,y:a}=s;c.moveTo(i+t*Math.cos(O[0]+e),a+t*Math.sin(O[0]+e)),O.forEach(r=>{c.lineTo(i+t*Math.cos(r+e),a+t*Math.sin(r+e))})}function yt(s,t){const{x:e,y:i}=s;c.arc(e,i,t,0,2*Math.PI)}const wt={classic:yt,hex:ft,triangular:gt};function xt(s,t){c.clearRect(0,0,x,x);const e=n.animate==="scale"||n.animate==="both",i=n.animate==="opacity"||n.animate==="both";s.forEach((a,r)=>{const{color:l,x:u,y:h,r:p,angleOffset:f}=a,L=t[r],g=e?L:1,S=i?L:1,m=wt[n.grid];c.globalAlpha=S,c.fillStyle=l,c.beginPath(),m({x:u*R+x*.5,y:x*.5-h*R},p*g,f||0),c.fill()})}const Lt=document.querySelector(".pulsar"),St=document.querySelector(".play-pause"),Et=[...document.querySelectorAll(".toggle-ui")];Et.forEach(s=>{s.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const vt=document.querySelector("input[name=autoplay]");class Rt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await X(this.grid.points,this.timeSinceLastRestart,n.code);if(v&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}xt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ct(n.grid),this.isPlaying=vt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),St.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Lt.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"}],_=V[Math.floor(Math.random()*V.length)],bt=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Mt=300,Pt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=y.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const i of bt)if(t.includes(i)){this.showError(`Let's play nice, no usage of "${i}" allowed.`);return}const e=await X(At,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});y.addEventListener("input",this.validate),y.addEventListener("change",this.validate),this.updateFromURL(),y.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){y.value=t,this.highlightCode()}update(t){y.value=t,this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",i=ht(e)||_.code;i.length>Mt?this.update("Code in the URL is too long."):this.update(i)}highlightCode(){let t=y.value;for(const[e,i]of Object.entries(Pt))t=t.replace(i,`$1`);Tt.innerHTML=t}showError(t){n.error=t,j.innerHTML=t}hideError(){n.error="",j.innerHTML=""}}class B{constructor(t,e,i){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=i,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(a=>{a.addEventListener("change",()=>{n.updateRadio(this.name,a.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const b=document.querySelector(".tutorial"),z=document.querySelector(".intro-text");let w=null;const d=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:z.innerText,code:""}];class qt{constructor(t){d[d.length-1].code=n.code,d[d.length-1].grid=n.grid,d[d.length-1].animate=n.animate,b.addEventListener("click",()=>{w===null?w=0:w=(w+1)%d.length,w===d.length-2?b.innerHTML="Finish":w===d.length-1?b.innerHTML="Restart":b.innerHTML="Next";const e=d[w];z.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const E=document.querySelector(".share");let M=0;E.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(M),E.classList.add("share--success"),M=setTimeout(()=>{E.classList.remove("share--success")},3e3))}catch(s){if(s.name==="AbortError")return;clearTimeout(M),E.classList.add("share--error"),M=setTimeout(()=>{E.classList.remove("share--error")},3e3)}});new B("grid",["classic","hex","triangular"],_.grid||"");new B("animate",["scale","opacity","both"],_.animate||"");new Ct;const Ot=new Rt;new qt(Ot); +var Q=Object.defineProperty;var Z=(s,t,e)=>t in s?Q(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>(Z(s,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const r of a)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function e(a){const r={};return a.integrity&&(r.integrity=a.integrity),a.referrerPolicy&&(r.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?r.credentials="include":a.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(a){if(a.ep)return;a.ep=!0;const r=e(a);fetch(a.href,r)}})();const W=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let $={};W.addEventListener("message",s=>{$[s.data.id](s.data),delete $[s.data.id]});async function X(s,t,e){return new Promise(i=>{const a=Math.random().toString(16)+Date.now().toString(16);$[a]=i,W.postMessage({id:a,grid:s,t,userCode:e})})}const T=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function D(s,t=1){const e=Math.sqrt(Math.pow(s.x*t,2)+Math.pow(s.y*t,2));return T[Math.floor(e)]||T[T.length-1]}const x=400,R=x*.075,tt=1.1,et=R*.85;function st(s,t){const e=[],i=.5*tt,a=2*i/Math.sqrt(3),r=i*2,l=a*1.5;for(let m=-t;m<=t;m+=1){const h=m*l,f=m%2!==0,g=f?i:0,L=f?1:0;for(let y=-s-L;y<=s;y+=1){const S=y*r+g,p=D({x:S,y:h});e.push({x:S,y:h,color:p,r:a*et})}}return e}const A=1.33,H=.8;function at(s,t){const e=[],a=1*Math.sqrt(3)/2,r=a/3*2*R,l=1/2,m=a;for(let h=-t;h<=t;h+=1){const f=h*m,g=h%2!==0,L=g?l:0,y=g?2:1,S=g?0:1;for(let p=-s-y;p<=s+S;p+=1){const k=p*l+L,G=p%2!==0,J=G?Math.PI:0,F=G?a/3:0,K=D({x:k,y:f-F},1/H);e.push({x:k*A,y:(f-F)*A,r:r*H*A,color:K,angleOffset:J})}}return e}const it=.88,ot=.5*it;function nt(s){const t=[];for(let e=-s;e<=s;e+=1)for(let i=-s;i<=s;i+=1){const a=D({x:i,y:e});t.push({x:i,y:e,r:ot*R,color:a})}return t}const rt={classic:()=>nt(6),hex:()=>st(5,6),triangular:()=>at(8,5)};class ct{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=rt[t]()}}let v=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!v).toString()),window.location.reload()};v&&document.body.classList.add("debug");const lt=["grid","animate","code"];function dt(s){return encodeURIComponent(btoa(s))}function ht(s){try{return atob(decodeURIComponent(s))}catch{return""}}function N(s){const t=new URLSearchParams(window.location.search);Object.entries(s).forEach(([a,r])=>{a==="code"?t.set(a,dt(r)):t.set(a,r)});for(const a of t.keys())lt.includes(a)||t.delete(a);const e=t.toString(),i=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",i)}const C={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ut{constructor(){o(this,"code",C.code);o(this,"grid",C.grid);o(this,"animate",C.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(a=>a(e)));const i=document.querySelector(`input[name=${t}][value=${e}]`);i.checked=!0,N({[t]:e})}updateCode(t){this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t})}}const n=new ut,I=document.querySelector(".canvas"),P=window.devicePixelRatio,c=I.getContext("2d");I.width=x*P;I.height=x*P;c.scale(P,P);const{PI:mt}=Math,U=Math.PI/6,Y=U*2,pt=Y*2,q=[0,1,2,3,4,5].map(s=>U+s*Y),O=[0,1,2].map(s=>mt+U+s*pt);function ft(s,t){const{x:e,y:i}=s;c.moveTo(e+t*Math.cos(q[0]),i+t*Math.sin(q[0])),q.forEach(a=>{c.lineTo(e+t*Math.cos(a),i+t*Math.sin(a))})}function gt(s,t,e){const{x:i,y:a}=s;c.moveTo(i+t*Math.cos(O[0]+e),a+t*Math.sin(O[0]+e)),O.forEach(r=>{c.lineTo(i+t*Math.cos(r+e),a+t*Math.sin(r+e))})}function yt(s,t){const{x:e,y:i}=s;c.arc(e,i,t,0,2*Math.PI)}const wt={classic:yt,hex:ft,triangular:gt};function xt(s,t){c.clearRect(0,0,x,x);const e=n.animate==="scale"||n.animate==="both",i=n.animate==="opacity"||n.animate==="both";s.forEach((a,r)=>{const{color:l,x:m,y:h,r:f,angleOffset:g}=a,L=t[r],y=e?L:1,S=i?L:1,p=wt[n.grid];c.globalAlpha=S,c.fillStyle=l,c.beginPath(),p({x:m*R+x*.5,y:x*.5-h*R},f*y,g||0),c.fill()})}const Lt=document.querySelector(".pulsar"),St=document.querySelector(".play-pause"),Et=[...document.querySelectorAll(".toggle-ui")];Et.forEach(s=>{s.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const vt=document.querySelector("input[name=autoplay]");class Rt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await X(this.grid.points,this.timeSinceLastRestart,n.code);if(v&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}xt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ct(n.grid),this.isPlaying=vt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),St.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Lt.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"},{code:"1 / abs((x + y) + (t * 4) % 70 - 35)"}],_=V[Math.floor(Math.random()*V.length)],bt=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Mt=300,Pt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=u.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const i of bt)if(t.includes(i)){this.showError(`Let's play nice, no usage of "${i}" allowed.`);return}const e=await X(At,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});u.addEventListener("input",this.validate),u.addEventListener("change",this.validate),this.updateFromURL(),u.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){u.value!==t&&(u.value=t,this.highlightCode())}update(t){u.value!==t&&(u.value=t),this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",i=ht(e)||_.code;i.length>Mt?this.update("Code in the URL is too long."):this.update(i)}highlightCode(){let t=u.value;for(const[e,i]of Object.entries(Pt))t=t.replace(i,`$1`);Tt.innerHTML=t}showError(t){n.error=t,j.innerHTML=t}hideError(){n.error="",j.innerHTML=""}}class B{constructor(t,e,i){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=i,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(a=>{a.addEventListener("change",()=>{n.updateRadio(this.name,a.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const b=document.querySelector(".tutorial"),z=document.querySelector(".intro-text");let w=null;const d=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:z.innerText,code:""}];class qt{constructor(t){d[d.length-1].code=n.code,d[d.length-1].grid=n.grid,d[d.length-1].animate=n.animate,b.addEventListener("click",()=>{w===null?w=0:w=(w+1)%d.length,w===d.length-2?b.innerHTML="Finish":w===d.length-1?b.innerHTML="Restart":b.innerHTML="Next";const e=d[w];z.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const E=document.querySelector(".share");let M=0;E.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(M),E.classList.add("share--success"),M=setTimeout(()=>{E.classList.remove("share--success")},3e3))}catch(s){if(s.name==="AbortError")return;clearTimeout(M),E.classList.add("share--error"),M=setTimeout(()=>{E.classList.remove("share--error")},3e3)}});new B("grid",["classic","hex","triangular"],_.grid||"");new B("animate",["scale","opacity","both"],_.animate||"");new Ct;const Ot=new Rt;new qt(Ot); diff --git a/docs/index.html b/docs/index.html index 62258ea..90bd3fb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -15,7 +15,7 @@ - + diff --git a/src/ts/lib/debug.ts b/src/ts/lib/debug.ts index 009d3c4..ba1553b 100644 --- a/src/ts/lib/debug.ts +++ b/src/ts/lib/debug.ts @@ -9,4 +9,10 @@ if (debug) { document.body.classList.add('debug'); } +export function log(...args: any[]) { + if (debug) { + console.log(...args); + } +} + export default debug; diff --git a/src/ts/lib/editor.ts b/src/ts/lib/editor.ts index 043180b..b6e0acf 100644 --- a/src/ts/lib/editor.ts +++ b/src/ts/lib/editor.ts @@ -98,13 +98,18 @@ export class Editor { // Only updates the value of the textarea, doesn't update the global state private updateValue(code: string) { - $editor.value = code; - this.highlightCode(); + if ($editor.value !== code) { + $editor.value = code; + this.highlightCode(); + } } // Updates the value of the textarea and the global state update(code: string) { - $editor.value = code; + if ($editor.value !== code) { + $editor.value = code; + } + this.validate(); } diff --git a/src/ts/lib/examples.ts b/src/ts/lib/examples.ts index 72c066e..b611555 100644 --- a/src/ts/lib/examples.ts +++ b/src/ts/lib/examples.ts @@ -47,6 +47,9 @@ const examples = [ code: '1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12', animate: 'opacity', }, + { + code: '1 / abs((x + y) + (t * 4) % 70 - 35)', + }, ]; export const randomExample = From eeff9d00d6b7e88c05938954764a6cd63818e869 Mon Sep 17 00:00:00 2001 From: Stanko Date: Thu, 2 Nov 2023 10:43:03 +0100 Subject: [PATCH 4/4] Better debug mode. Random example without refresh. --- docs/assets/index-2e5df5a1.css | 1 - docs/assets/index-3fbfee25.js | 2 ++ docs/assets/index-4822936d.css | 1 + docs/assets/index-fc504c8f.js | 1 - docs/index.html | 13 +++++--- src/css/pulsar.scss | 36 +++++++++++++++++++--- src/index.html | 9 ++++-- src/ts/app.ts | 7 +++-- src/ts/canvas-grid/index.ts | 3 -- src/ts/canvas-grid/svg.ts | 56 ---------------------------------- src/ts/lib/editor.ts | 4 +++ src/ts/lib/examples.ts | 15 +++++++-- src/ts/lib/pulsar.ts | 31 +++++++++++++++++-- src/ts/lib/random-example.ts | 18 +++++++++++ src/ts/lib/state.ts | 4 +++ src/ts/lib/types.ts | 4 +-- src/ts/lib/url.ts | 4 +++ 17 files changed, 126 insertions(+), 83 deletions(-) delete mode 100644 docs/assets/index-2e5df5a1.css create mode 100644 docs/assets/index-3fbfee25.js create mode 100644 docs/assets/index-4822936d.css delete mode 100644 docs/assets/index-fc504c8f.js delete mode 100644 src/ts/canvas-grid/svg.ts create mode 100644 src/ts/lib/random-example.ts diff --git a/docs/assets/index-2e5df5a1.css b/docs/assets/index-2e5df5a1.css deleted file mode 100644 index 57dd579..0000000 --- a/docs/assets/index-2e5df5a1.css +++ /dev/null @@ -1 +0,0 @@ -:root{line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;--bg-transparent: rgba(20, 34, 48, .85);--bg: rgb(20, 34, 48);--fg: #eee;--red: #ff2c55;--orange: #ff9500;--yellow: #ffcc02;--green: #35c759;--light-blue: #5bc7fa;--blue: #007aff;--purple: #5856d7;--mystic-magenta: #af52de;--red-contrast: hsl(348, 100%, 65%);--blue-contrast: hsl(211, 100%, 60%);--purple-contrast: hsl(241, 62%, 70%);--yellow-contrast: hsl(48, 100%, 60%);--text-opacity: .7}*{box-sizing:border-box;padding:0;margin:0;outline:none}*:focus-visible{outline:2px solid var(--purple-contrast)}svg{display:block}body{background-color:var(--bg);color:var(--fg);font-size:18px;font-family:Avenir,Montserrat,Corbel,URW Gothic,source-sans-pro,sans-serif}h1{font-weight:400;font-size:40px;line-height:1;letter-spacing:-.03em;color:var(--light-blue);font-family:ui-rounded,Hiragino Maru Gothic ProN,Quicksand,Comfortaa,Manjari,Arial Rounded MT,Arial Rounded MT Bold,Calibri,source-sans-pro,sans-serif;display:flex}@media (min-width: 600px){h1{font-size:48px}}h1 span:nth-child(1){color:var(--orange)}h1 span:nth-child(2){color:var(--yellow)}h1 span:nth-child(3){color:var(--green)}h1 span:nth-child(4){color:var(--light-blue)}h1 span:nth-child(5){color:var(--blue)}h1 span:nth-child(6){color:var(--purple)}a,a:visited{color:#dceaf7;text-decoration:none;outline:none}button{cursor:pointer}button,input{font:inherit}pre{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace}.pulsar{display:flex;flex-direction:column;min-height:100vh;min-height:100svh;max-width:1200px;margin:0 auto}@media (min-width: 1000px){.pulsar{flex-direction:row}}.top{width:100%}.title{display:flex;align-items:flex-end;margin-bottom:10px}.tutorial{margin-inline:8px auto}.intro-text{opacity:var(--text-opacity)}.icon-button,.button{background-color:#ffffff0d;border:none;color:#fff;border-radius:100px;font-size:16px;font-weight:700}.icon-button:hover,.button:hover{background-color:#ffffff26}.icon-button:active,.button:active{transform:translateY(1px)}.icon-button:focus-visible,.button:focus-visible{outline:2px solid var(--purple-contrast)}.icon-button{padding:10px;color:#ffffffb3}.icon-button:hover{color:#fff}.button{display:inline-flex;padding:10px 21px 10px 18px}.button svg{width:18px;height:18px}.button--sm{padding:3px 12px}.button span{display:inline-flex;align-items:center;gap:4px}.button:hover svg,.button:focus-visible svg{color:var(--purple-contrast)}.is-playing .play-pause__play-label,.is-paused .play-pause__pause-label{display:none}.canvas{display:block;width:100%}@media (min-width: 1000px){.canvas{margin-top:-10%}}.animation-wrapper{padding:30px;position:sticky;top:0;margin:auto;max-width:400px;width:100%}.animation{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center}@media (min-width: 1000px){.animation{margin-top:-10%}}.pixel-wrapper{width:7.5%;height:7.5%;position:relative}.pixel-wrapper[data-variant=triangular]{width:10%;height:10%}.pixel-wrapper svg{position:absolute;overflow:visible;will-change:transform;backface-visibility:hidden;transform:translateZ(0);width:100%;height:100%}@media (min-width: 600px){.toggle-ui{display:none}}.toggle-ui__absolute{display:none;position:absolute;top:10px;right:10px;z-index:1}@media (min-width: 600px){.toggle-ui__absolute{display:block}}.toggle-ui__show-label,.ui-hidden .controls{display:none}.ui-hidden .animation{margin-top:-10%}.ui-hidden .toggle-ui__absolute,.ui-hidden .toggle-ui__show-label{display:block}.ui-hidden .toggle-ui__hide-label{display:none}.controls{background-color:var(--bg-transparent);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);z-index:1;padding:30px;max-width:600px;width:100%;margin-inline:auto;border-radius:20px 20px 0 0;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;row-gap:15px}@media (min-width: 600px){.controls{padding:40px;row-gap:20px}}@media (min-width: 1000px){.controls{border-radius:0;width:450px;margin:0;padding:0 40px 40px;background:none;-webkit-backdrop-filter:none;backdrop-filter:none}}@media (min-width: 1300px){.controls{border-radius:0;width:600px;padding:0 60px 60px}}.radio__wrapper{display:flex;flex-wrap:wrap;column-gap:15px}.radio__name{margin-right:auto;flex:100% 0 0;font-size:16px;margin-bottom:4px;display:block}.radio__input{opacity:0;position:fixed;top:-500vh;left:-500vw;pointer-events:none;width:0;height:0}.radio__label{display:flex;align-items:center;gap:6px;border-radius:100px;padding-inline:5px;margin-inline:-5px;font-weight:700;font-size:16px;cursor:pointer}.radio__label:hover{text-decoration:underline}.radio__label svg:nth-child(2){display:none}.radio__input:focus-visible+.radio__label{outline:2px solid currentColor;outline-offset:2px}.radio__input:checked+.radio__label svg:nth-child(1){display:none}.radio__input:checked+.radio__label svg:nth-child(2){display:block}.radio__input--classic:checked+.radio__label{color:var(--green)}.radio__input--hex:checked+.radio__label{color:var(--light-blue)}.radio__input--triangular:checked+.radio__label{color:var(--blue-contrast)}.radio__input--scale:checked+.radio__label{color:var(--purple-contrast)}.radio__input--opacity:checked+.radio__label{color:var(--mystic-magenta)}.radio__input--both:checked+.radio__label{color:var(--red-contrast)}.radio__input--autoplay:checked+.radio__label{color:var(--yellow-contrast)}.editor{position:relative;width:100%}.editor__error{position:absolute;background-color:var(--bg);top:0;left:0;color:var(--orange);font-size:16px}.editor__label{font-size:16px;margin-bottom:4px;display:block}.editor__prefix{font-size:13px;margin-bottom:4px;opacity:var(--text-opacity)}.editor__wrapper{font-weight:400;text-align:left;width:100%;min-height:48px;position:relative;border-radius:10px;background:rgba(255,255,255,.05)}@media (min-width: 600px){.editor__wrapper{min-height:57px}}.editor__pre,.editor__textarea{overflow:hidden;width:100%;height:100%;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-size:17px;padding:10px 12px;border:none;display:block;line-height:1.6;border-radius:10px}@media (min-width: 600px){.editor__pre,.editor__textarea{font-size:18px;padding:14px 16px}}.editor__textarea{position:absolute;top:0;color:transparent;background:transparent;caret-color:#fff;resize:none}.editor__textarea .editor__textarea:focus{outline:4px solid rgba(255,255,255,.1)}.debug .editor__textarea{color:var(--light-blue);position:relative}.editor__pre{white-space:pre-wrap;word-wrap:break-word;overflow:hidden}.editor__pre i{font-style:normal}i.quotes{color:#e2d872}i.declaration{color:#64d5ea}i.fn{color:#a7e22e}i.parenthesis{color:#fed703}i.squared{color:#5fe8e8}i.curly{color:orchid}i.number{color:#9f77e7}i.logic{color:#f92572}i.equals{color:#5fe8e8}i.comments{color:#789;font-style:italic}i.comments>*{color:inherit}.bottom{display:flex;gap:8px;flex-wrap:wrap}.share .share__success-label{display:none}.share .share__success-label svg{color:var(--green)}.share .share__error-label,.share.share--success .share__default-label{display:none}.share.share--success .share__success-label{display:inline-flex}.share.share--error .share__default-label{display:none}.share.share--error .share__error-label{display:inline-flex} diff --git a/docs/assets/index-3fbfee25.js b/docs/assets/index-3fbfee25.js new file mode 100644 index 0000000..12ad40e --- /dev/null +++ b/docs/assets/index-3fbfee25.js @@ -0,0 +1,2 @@ +var tt=Object.defineProperty;var et=(a,t,e)=>t in a?tt(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e;var o=(a,t,e)=>(et(a,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const r of i)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&s(l)}).observe(document,{childList:!0,subtree:!0});function e(i){const r={};return i.integrity&&(r.integrity=i.integrity),i.referrerPolicy&&(r.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?r.credentials="include":i.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function s(i){if(i.ep)return;i.ep=!0;const r=e(i);fetch(i.href,r)}})();const R=document.querySelector(".share");let P=0;R.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(P),R.classList.add("share--success"),P=setTimeout(()=>{R.classList.remove("share--success")},3e3))}catch(a){if(a.name==="AbortError")return;clearTimeout(P),R.classList.add("share--error"),P=setTimeout(()=>{R.classList.remove("share--error")},3e3)}});let E=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!E).toString()),window.location.reload()};E&&document.body.classList.add("debug");function c(...a){E&&console.log(...a)}const at=["grid","animate","code"];function it(a){return encodeURIComponent(btoa(a))}function st(a){try{return atob(decodeURIComponent(a))}catch{return""}}function N(a){const t=new URLSearchParams(window.location.search);Object.entries(a).forEach(([i,r])=>{i==="code"?t.set(i,it(r)):t.set(i,r)});for(const i of t.keys())at.includes(i)||t.delete(i);const e=t.toString(),s=`${window.location.origin}${window.location.pathname}?${e}`;c("Updated URL:",JSON.stringify(a)),window.history.replaceState(null,"",s)}const A={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ot{constructor(){o(this,"code",A.code);o(this,"grid",A.grid);o(this,"animate",A.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?(c("Animate changed:",e),this.animate=e):t==="grid"&&(c("Grid changed:",e),this.grid=e),this.handlers[t].forEach(i=>i(e)));const s=document.querySelector(`input[name=${t}][value=${e}]`);s.checked=!0,N({[t]:e})}updateCode(t){c("Code changed:",t),this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t})}}const n=new ot,V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"},{code:"1 / abs((x + y) + (t * 4) % 70 - 35)"}];function X(){return{grid:"classic",animate:"scale",...V[Math.floor(Math.random()*V.length)]}}const U=X(),nt=document.querySelector(".random-example");nt.addEventListener("click",()=>{const a=X();n.updateCode(a.code),a.grid&&n.updateRadio("grid",a.grid),a.animate&&n.updateRadio("animate",a.animate)});const Y=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let D={};Y.addEventListener("message",a=>{D[a.data.id](a.data),delete D[a.data.id]});async function B(a,t,e){return new Promise(s=>{const i=Math.random().toString(16)+Date.now().toString(16);D[i]=s,Y.postMessage({id:i,grid:a,t,userCode:e})})}const C=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function _(a,t=1){const e=Math.sqrt(Math.pow(a.x*t,2)+Math.pow(a.y*t,2));return C[Math.floor(e)]||C[C.length-1]}const L=400,b=L*.075,rt=1.1,ct=b*.85;function dt(a,t){const e=[],s=.5*rt,i=2*s/Math.sqrt(3),r=s*2,l=i*1.5;for(let p=-t;p<=t;p+=1){const m=p*l,g=p%2!==0,y=g?s:0,S=g?1:0;for(let w=-a-S;w<=a;w+=1){const v=w*r+y,f=_({x:v,y:m});e.push({x:v,y:m,color:f,r:i*ct})}}return e}const q=1.33,j=.8;function lt(a,t){const e=[],i=1*Math.sqrt(3)/2,r=i/3*2*b,l=1/2,p=i;for(let m=-t;m<=t;m+=1){const g=m*p,y=m%2!==0,S=y?l:0,w=y?2:1,v=y?0:1;for(let f=-a-w;f<=a+v;f+=1){const k=f*l+S,G=f%2!==0,Q=G?Math.PI:0,F=G?i/3:0,Z=_({x:k,y:g-F},1/j);e.push({x:k*q,y:(g-F)*q,r:r*j*q,color:Z,angleOffset:Q})}}return e}const ut=.88,ht=.5*ut;function mt(a){const t=[];for(let e=-a;e<=a;e+=1)for(let s=-a;s<=a;s+=1){const i=_({x:s,y:e});t.push({x:s,y:e,r:ht*b,color:i})}return t}const pt={classic:()=>mt(6),hex:()=>dt(5,6),triangular:()=>lt(8,5)};class ft{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=pt[t]()}}const I=document.querySelector(".canvas"),T=window.devicePixelRatio,d=I.getContext("2d");I.width=L*T;I.height=L*T;d.scale(T,T);const{PI:gt}=Math,H=Math.PI/6,J=H*2,yt=J*2,$=[0,1,2,3,4,5].map(a=>H+a*J),O=[0,1,2].map(a=>gt+H+a*yt);function wt(a,t){const{x:e,y:s}=a;d.moveTo(e+t*Math.cos($[0]),s+t*Math.sin($[0])),$.forEach(i=>{d.lineTo(e+t*Math.cos(i),s+t*Math.sin(i))})}function xt(a,t,e){const{x:s,y:i}=a;d.moveTo(s+t*Math.cos(O[0]+e),i+t*Math.sin(O[0]+e)),O.forEach(r=>{d.lineTo(s+t*Math.cos(r+e),i+t*Math.sin(r+e))})}function Lt(a,t){const{x:e,y:s}=a;d.arc(e,s,t,0,2*Math.PI)}const St={classic:Lt,hex:wt,triangular:xt};function vt(a,t){d.clearRect(0,0,L,L);const e=n.animate==="scale"||n.animate==="both",s=n.animate==="opacity"||n.animate==="both";a.forEach((i,r)=>{const{color:l,x:p,y:m,r:g,angleOffset:y}=i,S=t[r],w=e?S:1,v=s?S:1,f=St[n.grid];d.globalAlpha=v,d.fillStyle=l,d.beginPath(),f({x:p*b+L*.5,y:L*.5-m*b},g*w,y||0),d.fill()})}const Et=document.querySelector(".pulsar"),Rt=document.querySelector(".play-pause"),bt=[...document.querySelectorAll(".toggle-ui")];bt.forEach(a=>{a.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const Pt=document.querySelector("input[name=autoplay]"),Mt=document.querySelector(".fps"),Tt=document.querySelector(".fps__value");class At{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsHistory",[]);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await B(this.grid.points,this.timeSinceLastRestart,n.code);if(E&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(this.fpsHistory.push(this.fps),this.fpsHistory.length>60&&this.fpsHistory.shift(),Mt.innerHTML=this.fpsHistory.map(e=>`
`).join(` +`),Tt.innerHTML=this.fps.toString(),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}vt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?(c("Play or draw - start/resume animation"),this.animate()):(c("Play or draw - draw a single frame"),this.draw())});this.grid=new ft(n.grid),this.isPlaying=Pt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),Rt.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(c("Tab went inactive"),this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&(c("Tab is active again - resuming"),this.play())})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Et.setAttribute("class",t.join(" "))}play(){if(this.isPlaying){c("Play, already playing");return}c("Play"),this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),E&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,c("Pause on error"),E&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){c("Pause"),this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const Ct=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],qt=300,$t={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=u.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const s of Ct)if(t.includes(s)){this.showError(`Let's play nice, no usage of "${s}" allowed.`);return}const e=await B(Dt,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});u.addEventListener("input",this.validate),u.addEventListener("change",this.validate),this.updateFromURL(),u.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){u.value!==t&&(u.value=t,this.highlightCode(),c("Editor, code updated"))}update(t){u.value!==t&&(u.value=t,c("Editor, code updated and validated")),this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",s=st(e)||U.code;s.length>qt?this.update("Code in the URL is too long."):this.update(s)}highlightCode(){let t=u.value;for(const[e,s]of Object.entries($t))t=t.replace(s,`$1`);Ot.innerHTML=t}showError(t){c(`Error: %c${t}`,"color: red","code:",u.value),n.error=t,W.innerHTML=t}hideError(){n.error="",W.innerHTML=""}}class z{constructor(t,e,s){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=s,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(i=>{i.addEventListener("change",()=>{n.updateRadio(this.name,i.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const M=document.querySelector(".tutorial"),K=document.querySelector(".intro-text");let x=null;const h=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:K.innerText,code:""}];class _t{constructor(t){h[h.length-1].code=n.code,h[h.length-1].grid=n.grid,h[h.length-1].animate=n.animate,M.addEventListener("click",()=>{x===null?x=0:x=(x+1)%h.length,x===h.length-2?M.innerHTML="Finish":x===h.length-1?M.innerHTML="Restart":M.innerHTML="Next";const e=h[x];K.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}new z("grid",["classic","hex","triangular"],U.grid||"");new z("animate",["scale","opacity","both"],U.animate||"");new Ut;const It=new At;new _t(It); diff --git a/docs/assets/index-4822936d.css b/docs/assets/index-4822936d.css new file mode 100644 index 0000000..e0e295a --- /dev/null +++ b/docs/assets/index-4822936d.css @@ -0,0 +1 @@ +.fps{display:flex;position:fixed;top:0;left:0;height:40px;width:60px;align-items:flex-end;justify-content:flex-end}.fps__bar{flex:1px 0 0;background-color:var(--green)}.fps__value{position:fixed;top:40px;left:0;font-size:12px;padding:2px 4px}:root{line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-text-size-adjust:100%;--bg-transparent: rgba(20, 34, 48, .85);--bg: rgb(20, 34, 48);--fg: #eee;--red: #ff2c55;--orange: #ff9500;--yellow: #ffcc02;--green: #35c759;--light-blue: #5bc7fa;--blue: #007aff;--purple: #5856d7;--mystic-magenta: #af52de;--red-contrast: hsl(348, 100%, 65%);--blue-contrast: hsl(211, 100%, 60%);--purple-contrast: hsl(241, 62%, 70%);--yellow-contrast: hsl(48, 100%, 60%);--text-opacity: .7}*{box-sizing:border-box;padding:0;margin:0;outline:none}*:focus-visible{outline:2px solid var(--purple-contrast)}svg{display:block}body{background-color:var(--bg);color:var(--fg);font-size:18px;font-family:Avenir,Montserrat,Corbel,URW Gothic,source-sans-pro,sans-serif}h1{font-weight:400;font-size:40px;line-height:1;letter-spacing:-.03em;color:var(--light-blue);font-family:ui-rounded,Hiragino Maru Gothic ProN,Quicksand,Comfortaa,Manjari,Arial Rounded MT,Arial Rounded MT Bold,Calibri,source-sans-pro,sans-serif;display:flex}@media (min-width: 600px){h1{font-size:48px}}h1 span:nth-child(1){color:var(--orange)}h1 span:nth-child(2){color:var(--yellow)}h1 span:nth-child(3){color:var(--green)}h1 span:nth-child(4){color:var(--light-blue)}h1 span:nth-child(5){color:var(--blue)}h1 span:nth-child(6){color:var(--purple)}a,a:visited{color:#dceaf7;text-decoration:none;outline:none}button{cursor:pointer}button,input{font:inherit}pre{font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace}.pulsar{display:flex;flex-direction:column;min-height:100vh;min-height:100svh;max-width:1200px;margin:0 auto}@media (min-width: 1000px){.pulsar{flex-direction:row}}.top{width:100%}.title{display:flex;align-items:flex-end;margin-bottom:10px}.tutorial{margin-inline:8px auto}.intro-text{opacity:var(--text-opacity)}.icon-button,.button{background-color:#ffffff0d;border:none;color:#fff;border-radius:100px;font-size:16px;font-weight:700}.icon-button:hover,.button:hover{background-color:#ffffff26}.icon-button:active,.button:active{transform:translateY(1px)}.icon-button:focus-visible,.button:focus-visible{outline:2px solid var(--purple-contrast)}.icon-button{padding:10px;color:#ffffffb3}.icon-button:hover{color:#fff}.button{display:inline-flex;padding:10px 21px 10px 18px}.button svg{width:18px;height:18px}.button--sm{padding:3px 12px}.button span{display:inline-flex;align-items:center;gap:4px}.button:hover svg,.button:focus-visible svg{color:var(--purple-contrast)}.is-playing .play-pause__play-label,.is-paused .play-pause__pause-label{display:none}.canvas{display:block;width:100%}@media (min-width: 1000px){.canvas{margin-top:-10%}}.animation-wrapper{padding:30px;position:sticky;top:0;margin:auto;max-width:400px;width:100%}.animation{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center}@media (min-width: 1000px){.animation{margin-top:-10%}}.pixel-wrapper{width:7.5%;height:7.5%;position:relative}.pixel-wrapper[data-variant=triangular]{width:10%;height:10%}.pixel-wrapper svg{position:absolute;overflow:visible;will-change:transform;backface-visibility:hidden;transform:translateZ(0);width:100%;height:100%}@media (min-width: 600px){.toggle-ui{display:none}}.toggle-ui__absolute{display:none;position:absolute;top:10px;right:10px;z-index:1}@media (min-width: 600px){.toggle-ui__absolute{display:block}}.toggle-ui__show-label,.ui-hidden .controls{display:none}.ui-hidden .animation{margin-top:-10%}.ui-hidden .toggle-ui__absolute,.ui-hidden .toggle-ui__show-label{display:block}.ui-hidden .toggle-ui__hide-label{display:none}.controls{background-color:var(--bg-transparent);backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px);z-index:1;padding:30px;max-width:600px;width:100%;margin-inline:auto;border-radius:20px 20px 0 0;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;row-gap:15px}@media (min-width: 600px){.controls{padding:40px;row-gap:20px}}@media (min-width: 1000px){.controls{border-radius:0;width:450px;margin:0;padding:0 40px 40px;background:none;-webkit-backdrop-filter:none;backdrop-filter:none}}@media (min-width: 1300px){.controls{border-radius:0;width:600px;padding:0 60px 60px}}.radio__wrapper{display:flex;flex-wrap:wrap;column-gap:15px}.radio__name{margin-right:auto;flex:100% 0 0;font-size:16px;margin-bottom:4px;display:block}.radio__input{opacity:0;position:fixed;top:-500vh;left:-500vw;pointer-events:none;width:0;height:0}.radio__label{display:flex;align-items:center;gap:6px;border-radius:100px;padding-inline:5px;margin-inline:-5px;font-weight:700;font-size:16px;cursor:pointer}.radio__label:hover{text-decoration:underline}.radio__label svg:nth-child(2){display:none}.radio__input:focus-visible+.radio__label{outline:2px solid currentColor;outline-offset:2px}.radio__input:checked+.radio__label svg:nth-child(1){display:none}.radio__input:checked+.radio__label svg:nth-child(2){display:block}.radio__input--classic:checked+.radio__label{color:var(--green)}.radio__input--hex:checked+.radio__label{color:var(--light-blue)}.radio__input--triangular:checked+.radio__label{color:var(--blue-contrast)}.radio__input--scale:checked+.radio__label{color:var(--purple-contrast)}.radio__input--opacity:checked+.radio__label{color:var(--mystic-magenta)}.radio__input--both:checked+.radio__label{color:var(--red-contrast)}.radio__input--autoplay:checked+.radio__label{color:var(--yellow-contrast)}.editor{position:relative;width:100%}.editor__error{position:absolute;background-color:var(--bg);top:0;left:0;color:var(--orange);font-size:16px}.editor__label{font-size:16px;margin-bottom:4px;display:block}.editor__prefix{font-size:13px;margin-bottom:4px;opacity:var(--text-opacity)}.editor__wrapper{font-weight:400;text-align:left;width:100%;min-height:48px;position:relative;border-radius:10px;background:rgba(255,255,255,.05)}@media (min-width: 600px){.editor__wrapper{min-height:57px}}.editor__pre,.editor__textarea{overflow:hidden;width:100%;height:100%;font-family:ui-monospace,Cascadia Code,Source Code Pro,Menlo,Consolas,DejaVu Sans Mono,monospace;font-size:17px;padding:10px 12px;border:none;display:block;line-height:1.6;border-radius:10px}@media (min-width: 600px){.editor__pre,.editor__textarea{font-size:18px;padding:14px 16px}}.editor__textarea{position:absolute;top:0;color:transparent;background:transparent;caret-color:#fff;resize:none}.debug .editor__textarea{color:var(--fg);position:relative}.debug .editor__textarea:not(:focus){outline:1px solid rgba(255,255,255,.1)}.editor__pre{white-space:pre-wrap;word-wrap:break-word;overflow:hidden}.editor__pre i{font-style:normal}i.quotes{color:#e2d872}i.declaration{color:#64d5ea}i.fn{color:#a7e22e}i.parenthesis{color:#fed703}i.squared{color:#5fe8e8}i.curly{color:orchid}i.number{color:#9f77e7}i.logic{color:#f92572}i.equals{color:#5fe8e8}i.comments{color:#789;font-style:italic}i.comments>*{color:inherit}.bottom{display:flex;gap:8px;flex-wrap:wrap}.share .share__success-label{display:none}.share .share__success-label svg{color:var(--green)}.share .share__error-label,.share.share--success .share__default-label{display:none}.share.share--success .share__success-label{display:inline-flex}.share.share--error .share__default-label{display:none}.share.share--error .share__error-label{display:inline-flex} diff --git a/docs/assets/index-fc504c8f.js b/docs/assets/index-fc504c8f.js deleted file mode 100644 index 6298cd7..0000000 --- a/docs/assets/index-fc504c8f.js +++ /dev/null @@ -1 +0,0 @@ -var Q=Object.defineProperty;var Z=(s,t,e)=>t in s?Q(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var o=(s,t,e)=>(Z(s,typeof t!="symbol"?t+"":t,e),e);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const r of a)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&i(l)}).observe(document,{childList:!0,subtree:!0});function e(a){const r={};return a.integrity&&(r.integrity=a.integrity),a.referrerPolicy&&(r.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?r.credentials="include":a.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(a){if(a.ep)return;a.ep=!0;const r=e(a);fetch(a.href,r)}})();const W=new Worker(new URL("/pulsar/assets/worker-b40f7b8b.js",self.location));let $={};W.addEventListener("message",s=>{$[s.data.id](s.data),delete $[s.data.id]});async function X(s,t,e){return new Promise(i=>{const a=Math.random().toString(16)+Date.now().toString(16);$[a]=i,W.postMessage({id:a,grid:s,t,userCode:e})})}const T=["#ff9500","#ffcc02","#35c759","#5bc7fa","#007aff","#5856d7","#af52de","#ff2c55"];function D(s,t=1){const e=Math.sqrt(Math.pow(s.x*t,2)+Math.pow(s.y*t,2));return T[Math.floor(e)]||T[T.length-1]}const x=400,R=x*.075,tt=1.1,et=R*.85;function st(s,t){const e=[],i=.5*tt,a=2*i/Math.sqrt(3),r=i*2,l=a*1.5;for(let m=-t;m<=t;m+=1){const h=m*l,f=m%2!==0,g=f?i:0,L=f?1:0;for(let y=-s-L;y<=s;y+=1){const S=y*r+g,p=D({x:S,y:h});e.push({x:S,y:h,color:p,r:a*et})}}return e}const A=1.33,H=.8;function at(s,t){const e=[],a=1*Math.sqrt(3)/2,r=a/3*2*R,l=1/2,m=a;for(let h=-t;h<=t;h+=1){const f=h*m,g=h%2!==0,L=g?l:0,y=g?2:1,S=g?0:1;for(let p=-s-y;p<=s+S;p+=1){const k=p*l+L,G=p%2!==0,J=G?Math.PI:0,F=G?a/3:0,K=D({x:k,y:f-F},1/H);e.push({x:k*A,y:(f-F)*A,r:r*H*A,color:K,angleOffset:J})}}return e}const it=.88,ot=.5*it;function nt(s){const t=[];for(let e=-s;e<=s;e+=1)for(let i=-s;i<=s;i+=1){const a=D({x:i,y:e});t.push({x:i,y:e,r:ot*R,color:a})}return t}const rt={classic:()=>nt(6),hex:()=>st(5,6),triangular:()=>at(8,5)};class ct{constructor(t="classic"){o(this,"type","classic");o(this,"points",[]);this.type=t,this.update(t)}update(t="classic"){this.type=t,this.points=rt[t]()}}let v=localStorage.getItem("pulsar:debug")==="true";window.debug=()=>{localStorage.setItem("pulsar:debug",(!v).toString()),window.location.reload()};v&&document.body.classList.add("debug");const lt=["grid","animate","code"];function dt(s){return encodeURIComponent(btoa(s))}function ht(s){try{return atob(decodeURIComponent(s))}catch{return""}}function N(s){const t=new URLSearchParams(window.location.search);Object.entries(s).forEach(([a,r])=>{a==="code"?t.set(a,dt(r)):t.set(a,r)});for(const a of t.keys())lt.includes(a)||t.delete(a);const e=t.toString(),i=`${window.location.origin}${window.location.pathname}?${e}`;window.history.replaceState(null,"",i)}const C={code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"};class ut{constructor(){o(this,"code",C.code);o(this,"grid",C.grid);o(this,"animate",C.animate);o(this,"error","");o(this,"handlers",{code:[],grid:[],animate:[]})}onChange(t,e){this.handlers[t].push(e)}updateRadio(t,e){this[t]!==e&&(t==="animate"?this.animate=e:t==="grid"&&(this.grid=e),this.handlers[t].forEach(a=>a(e)));const i=document.querySelector(`input[name=${t}][value=${e}]`);i.checked=!0,N({[t]:e})}updateCode(t){this.code=t,this.handlers.code.forEach(e=>e(t)),N({code:t})}}const n=new ut,I=document.querySelector(".canvas"),P=window.devicePixelRatio,c=I.getContext("2d");I.width=x*P;I.height=x*P;c.scale(P,P);const{PI:mt}=Math,U=Math.PI/6,Y=U*2,pt=Y*2,q=[0,1,2,3,4,5].map(s=>U+s*Y),O=[0,1,2].map(s=>mt+U+s*pt);function ft(s,t){const{x:e,y:i}=s;c.moveTo(e+t*Math.cos(q[0]),i+t*Math.sin(q[0])),q.forEach(a=>{c.lineTo(e+t*Math.cos(a),i+t*Math.sin(a))})}function gt(s,t,e){const{x:i,y:a}=s;c.moveTo(i+t*Math.cos(O[0]+e),a+t*Math.sin(O[0]+e)),O.forEach(r=>{c.lineTo(i+t*Math.cos(r+e),a+t*Math.sin(r+e))})}function yt(s,t){const{x:e,y:i}=s;c.arc(e,i,t,0,2*Math.PI)}const wt={classic:yt,hex:ft,triangular:gt};function xt(s,t){c.clearRect(0,0,x,x);const e=n.animate==="scale"||n.animate==="both",i=n.animate==="opacity"||n.animate==="both";s.forEach((a,r)=>{const{color:l,x:m,y:h,r:f,angleOffset:g}=a,L=t[r],y=e?L:1,S=i?L:1,p=wt[n.grid];c.globalAlpha=S,c.fillStyle=l,c.beginPath(),p({x:m*R+x*.5,y:x*.5-h*R},f*y,g||0),c.fill()})}const Lt=document.querySelector(".pulsar"),St=document.querySelector(".play-pause"),Et=[...document.querySelectorAll(".toggle-ui")];Et.forEach(s=>{s.addEventListener("click",()=>{document.body.classList.toggle("ui-hidden")})});const vt=document.querySelector("input[name=autoplay]");class Rt{constructor(){o(this,"isPlaying",!1);o(this,"wasPlaying",!1);o(this,"raf",0);o(this,"time",0);o(this,"lastRestart",Date.now());o(this,"timeSinceLastRestart",0);o(this,"fps",0);o(this,"fpsStartTime",Date.now());o(this,"grid");o(this,"draw",async()=>{if(cancelAnimationFrame(this.raf),n.error!==""){this.pauseOnError();return}const t=await X(this.grid.points,this.timeSinceLastRestart,n.code);if(v&&(this.fps++,Date.now()-this.fpsStartTime>1e3&&(console.log("fps",this.fps),this.fpsStartTime=Date.now(),this.fps=0)),t.error){this.pauseOnError();return}xt(this.grid.points,t.data),this.updateRootClass(),this.isPlaying&&(this.raf=requestAnimationFrame(this.animate))});o(this,"animate",()=>{this.timeSinceLastRestart=this.time+(Date.now()-this.lastRestart)/200,this.draw()});o(this,"playOrDraw",()=>{this.isPlaying?this.animate():this.draw()});this.grid=new ct(n.grid),this.isPlaying=vt.checked,this.playOrDraw(),n.onChange("grid",t=>{this.grid.update(t),this.playOrDraw()}),n.onChange("animate",this.playOrDraw),n.onChange("code",()=>{this.playOrDraw()}),St.addEventListener("click",()=>{this.isPlaying?this.pause():this.play()}),window.addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"?(this.wasPlaying=this.isPlaying,this.pause()):document.visibilityState==="visible"&&this.wasPlaying&&this.play()})}updateRootClass(){const t=["pulsar"];this.isPlaying?t.push("is-playing"):t.push("is-paused"),Lt.setAttribute("class",t.join(" "))}play(){this.isPlaying||(this.isPlaying=!0,this.lastRestart=Date.now(),this.animate(),v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass())}pauseOnError(){cancelAnimationFrame(this.raf),this.time=this.timeSinceLastRestart,v&&(this.fpsStartTime=Date.now(),this.fps=0),this.updateRootClass()}pause(){this.isPlaying=!1,this.time=this.timeSinceLastRestart,cancelAnimationFrame(this.raf),this.updateRootClass()}}const V=[{code:"(cos(x * t / 5) + sin(y * t / 5)) / 2"},{code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"hex"},{code:"(cos(sin(x * y) + t * 0.66) + 1) / 2"},{code:"((cos(t + x + cos(t)) + sin(t + y)) + 2) / 4"},{code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1"},{code:"cos(x + t) > y * 0.3 + 0.5 ? (cos(x + t) + 1) / 4 + 0.5 : 0"},{code:"cos(t * 0.5) * 0.5 + 0.5",animate:"opacity"},{code:"sin(0.5 * x + y * t * 0.8) * sin(t / 4)",grid:"triangular"},{code:"sin(x * cos(t * 0.5)) + cos(y * sin(t * 0.5))",grid:"hex"},{code:" sin(t) * sin(x) + cos(t) * cos(y)",grid:"triangular",animate:"opacity"},{code:"abs(abs(x) - abs(y)) < t % 7 ? 1 : 0",grid:"hex"},{code:"sin(3 * atan2(y,x) + t)",grid:"hex"},{code:"1 - (((x + 3) * (x + 4) + y + t * 0.3 * (1 + x * x % 5) * 3) % 36) / 12",animate:"opacity"},{code:"1 / abs((x + y) + (t * 4) % 70 - 35)"}],_=V[Math.floor(Math.random()*V.length)],bt=["fetch","import","export","eval","Function","window","self","top","location","document","alert","prompt","confirm","XMLHttpRequest","setTimeout","setInterval","clearTimeout","clearInterval","requestAnimationFrame","cancelAnimationFrame","postMessage","addEventListener","removeEventListener","dispatchEvent","localStorage","sessionStorage","indexedDB","Geolocation","await","async","WebSocket","Worker","postMessage","importScripts","URL","console","debugger"],Mt=300,Pt={equals:/(\b=\b)/g,quotes:new RegExp(`(('.*?')|(".*?")|(".*?(?{clearTimeout(this.timeout);const t=u.value.trim();if(this.highlightCode(),t===""){this.showError("Type some code to get started.");return}for(const i of bt)if(t.includes(i)){this.showError(`Let's play nice, no usage of "${i}" allowed.`);return}const e=await X(At,0,t);if(e.error){this.showError(e.error);return}this.hideError(),n.updateCode(t)});u.addEventListener("input",this.validate),u.addEventListener("change",this.validate),this.updateFromURL(),u.addEventListener("keydown",t=>{t.key=="Enter"&&t.preventDefault()}),n.onChange("code",t=>this.updateValue(t))}updateValue(t){u.value!==t&&(u.value=t,this.highlightCode())}update(t){u.value!==t&&(u.value=t),this.validate()}updateFromURL(){const e=new URLSearchParams(window.location.search).get("code")||"",i=ht(e)||_.code;i.length>Mt?this.update("Code in the URL is too long."):this.update(i)}highlightCode(){let t=u.value;for(const[e,i]of Object.entries(Pt))t=t.replace(i,`$1`);Tt.innerHTML=t}showError(t){n.error=t,j.innerHTML=t}hideError(){n.error="",j.innerHTML=""}}class B{constructor(t,e,i){o(this,"$inputs");o(this,"name");o(this,"options");o(this,"initialValue");this.name=t,this.options=e,this.initialValue=i,this.$inputs=[...document.querySelectorAll(`input[name=${t}]`)],this.$inputs.forEach(a=>{a.addEventListener("change",()=>{n.updateRadio(this.name,a.value)})}),this.updateFromURL()}updateFromURL(){let e=new URLSearchParams(window.location.search).get(this.name)||this.initialValue;this.options.includes(e)||(e=this.options[0]),n.updateRadio(this.name,e)}update(t){n.updateRadio(this.name,t)}}const b=document.querySelector(".tutorial"),z=document.querySelector(".intro-text");let w=null;const d=[{text:"Pulsar allows you to create animations using code.",code:"sin(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:"Write code that returns a value between 0 and 1. Try changing the code below.",code:"0.5 ",grid:"classic",animate:"scale"},{text:'The code is evaluated for each "pixel" individually.',code:"random()",grid:"classic",animate:"scale"},{text:'Parameter "t" is time in fifth of a second.',code:"cos(t) * 0.5 + 0.5",grid:"classic",animate:"scale"},{text:'You can speed up or slow down the animation by multiplying the "t". To make it loop use the "%" operator.',code:"(t * 0.1) % 1",grid:"classic",animate:"scale"},{text:'Parameters "x" and "y" are coordinates and they span roughly from -6 to 6.',code:"abs(x / 12) + abs(y / 12)",grid:"classic",animate:"scale"},{text:'Parameter "i" is the index of the pixel in the grid.',code:"i % 2"},{text:'You can use any of the mathematical functions available in JavaScript like "cos" or "sqrt".',code:"cos(x) + sin(y)",grid:"classic",animate:"scale"},{text:"Simplex noise is also available.",code:"noise(x + t, y + t)",grid:"classic",animate:"scale"},{text:"You can switch between different grid types and animation properties.",code:"cos(x + t) * 0.5 + 0.5",grid:"hex",animate:"both"},{text:"Here are a few examples to get you started. Radial wave:",code:"(cos(sqrt(x * x + y * y) - t) + 1) / 2",grid:"classic",animate:"scale"},{text:"Liner cosine wave:",code:"cos(x * 0.8 + t) > y * 0.25 ? 1 : 0",grid:"classic",animate:"scale"},{text:'"Floral" pattern:',code:"(cos(sin(x * y) + t * 0.66) + 1) / 2",grid:"classic",animate:"scale"},{text:"Experiment combining different functions and parameters to create funky animations. Have fun!",code:"sqrt(x*x + y*y) > (cos(x + t) + 1) / 2 * 5 ? noise(x + t, y + t) * 0.3 : 1",grid:"classic",animate:"scale"},{text:z.innerText,code:""}];class qt{constructor(t){d[d.length-1].code=n.code,d[d.length-1].grid=n.grid,d[d.length-1].animate=n.animate,b.addEventListener("click",()=>{w===null?w=0:w=(w+1)%d.length,w===d.length-2?b.innerHTML="Finish":w===d.length-1?b.innerHTML="Restart":b.innerHTML="Next";const e=d[w];z.innerHTML=e.text,n.updateCode(e.code),(e.grid||e.animate)&&(e.grid&&n.updateRadio("grid",e.grid),e.animate&&n.updateRadio("animate",e.animate)),t.isPlaying||t.play()})}}const E=document.querySelector(".share");let M=0;E.addEventListener("click",async()=>{try{navigator.share?await navigator.share({title:"Check my Pulsar animation",url:window.location.href}):(await navigator.clipboard.writeText(window.location.href),clearTimeout(M),E.classList.add("share--success"),M=setTimeout(()=>{E.classList.remove("share--success")},3e3))}catch(s){if(s.name==="AbortError")return;clearTimeout(M),E.classList.add("share--error"),M=setTimeout(()=>{E.classList.remove("share--error")},3e3)}});new B("grid",["classic","hex","triangular"],_.grid||"");new B("animate",["scale","opacity","both"],_.animate||"");new Ct;const Ot=new Rt;new qt(Ot); diff --git a/docs/index.html b/docs/index.html index 90bd3fb..b23394f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -15,12 +15,17 @@ - - + + +
+
+
+  
+