├── .gitattributes ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app ├── app.css ├── index.html ├── index_build.html └── manifest.json ├── build.sh ├── build ├── app.css ├── app.js ├── index.html └── manifest.json ├── out └── .gitignore ├── package.json ├── tsconfig.json └── typescript ├── Background.ts ├── BigBrother.ts ├── Box.ts ├── Firefox.ts ├── FiringPin.ts ├── InternetExplorer.ts ├── Level.ts ├── Main.ts ├── MovingWebsite.ts ├── NoWebsite.ts ├── Reticle.ts ├── UserAgent.ts ├── Wall.ts ├── Website.ts ├── WebsiteBox.ts ├── audio ├── audio.ts ├── oborona.ts └── reverbgen.ts ├── js13k2020.d.ts ├── levels ├── Level_02_TheWall.ts ├── Level_03_Opening.ts ├── Level_04_Breach.ts ├── Level_05_Banned.ts ├── Level_06_Reversal.ts ├── Level_07_Moving.ts ├── Level_08_Distancing.ts ├── Level_09_Hopeless.ts └── Level_10_End.ts ├── natlib ├── Canvas.ts ├── Mainloop.ts ├── NBall.ts ├── NBody.ts ├── NConstraint.ts ├── NScene.ts ├── NStaticVertex.ts ├── NVec2.ts ├── NVertex.ts ├── Pointer.ts ├── Prelude.ts ├── SAT.ts ├── Utils.ts └── natlib.d.ts ├── webmonetization.d.ts └── zzfx.d.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | build/* linguist-generated=true 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "sound/ZzFX"] 2 | path = sound/ZzFX 3 | url = https://github.com/mvasilkov/ZzFX.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Fuck Around and Find Out License 2 | 3 | Copyright (c) 2020 Mark Vasilkov (https://github.com/mvasilkov) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | This Software MUST BE USED FOR GOOD, AND MUST NOT BE USED FOR EVIL. 16 | The original author of the Software RETAINS THE SOLE AND EXCLUSIVE RIGHT TO 17 | DETERMINE WHICH USES ARE GOOD AND WHICH USES ARE EVIL. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | I want to google the game 2 | === 3 | 4 | Written by Mark Vasilkov for js13kGames in 2020. 5 | 6 | Physics code is based on work by Benedikt Bitterli and Gerard Ferrandez. 7 | 8 | Sound effects use ZzFX by Frank Force. 9 | -------------------------------------------------------------------------------- /app/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | -ms-touch-action: manipulation; 3 | touch-action: manipulation; 4 | -moz-user-select: none; 5 | -moz-user-select: -moz-none; 6 | -ms-user-select: none; 7 | -webkit-touch-callout: none; 8 | -webkit-user-select: none; 9 | user-select: none; 10 | } 11 | 12 | body { 13 | align-items: center; 14 | background: #101010; 15 | display: flex; 16 | height: 100vh; 17 | justify-content: center; 18 | margin: 0; 19 | overflow: hidden; 20 | -webkit-text-size-adjust: none; 21 | text-size-adjust: none; 22 | width: 100vw; 23 | } 24 | 25 | .box {} 26 | 27 | .can { 28 | background: #000; 29 | display: block; 30 | height: 540px; 31 | outline: 1px solid #202020; 32 | -ms-touch-action: none; 33 | touch-action: none; 34 | -webkit-transform: scale3d(1, 1, 1); 35 | transform: scale3d(1, 1, 1); 36 | width: 960px; 37 | } 38 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | I want to google the game 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /app/index_build.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | I want to google the game 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "I want to google the game", 3 | "short_name": "js13k-2020", 4 | "display": "standalone", 5 | "orientation": "landscape" 6 | } 7 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf build 4 | mkdir -p build 5 | 6 | npx html-minifier --collapse-whitespace --remove-attribute-quotes \ 7 | -o build/index.html app/index_build.html 8 | 9 | npx cleancss -O1 -o build/app.css app/app.css 10 | 11 | npx terser --enclose --compress --mangle -o build/app.js \ 12 | sound/ZzFX/ZzFX.js out/audio/reverbgen.js out/audio/audio.js out/audio/oborona.js \ 13 | out/natlib/NVec2.js out/natlib/Utils.js out/natlib/Prelude.js out/natlib/NVertex.js \ 14 | out/natlib/NStaticVertex.js out/natlib/NConstraint.js out/natlib/NBody.js out/natlib/SAT.js \ 15 | out/natlib/NBall.js out/natlib/NScene.js out/natlib/Canvas.js out/natlib/Pointer.js \ 16 | out/natlib/Mainloop.js out/Reticle.js out/FiringPin.js out/UserAgent.js out/Firefox.js \ 17 | out/InternetExplorer.js out/BigBrother.js out/Wall.js out/Box.js out/WebsiteBox.js out/Website.js \ 18 | out/MovingWebsite.js out/NoWebsite.js out/Level.js out/levels/Level_02_TheWall.js \ 19 | out/levels/Level_03_Opening.js out/levels/Level_04_Breach.js out/levels/Level_05_Banned.js \ 20 | out/levels/Level_06_Reversal.js out/levels/Level_07_Moving.js out/levels/Level_08_Distancing.js \ 21 | out/levels/Level_09_Hopeless.js out/levels/Level_10_End.js out/Background.js out/Main.js 22 | 23 | python3 -c "import json; from pathlib import Path; "\ 24 | "obj = json.loads(Path('app/manifest.json').read_text(encoding='utf-8')); "\ 25 | "Path('build/manifest.json').write_text(json.dumps(obj, separators=(',', ':')), encoding='utf-8')" 26 | -------------------------------------------------------------------------------- /build/app.css: -------------------------------------------------------------------------------- 1 | *{-ms-touch-action:manipulation;touch-action:manipulation;-moz-user-select:none;-moz-user-select:-moz-none;-ms-user-select:none;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}body{align-items:center;background:#101010;display:flex;height:100vh;justify-content:center;margin:0;overflow:hidden;-webkit-text-size-adjust:none;text-size-adjust:none;width:100vw}.can{background:#000;display:block;height:540px;outline:1px solid #202020;-ms-touch-action:none;touch-action:none;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1);width:960px} -------------------------------------------------------------------------------- /build/app.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";let t=44100,e=(e=1,s=.05,i=220,n=0,r=0,a=.1,o=0,c=1,l=0,h=0,f=0,d=0,u=0,x=0,p=0,g=0,y=0,v=1,w=0,b=0)=>{let M,S,k=2*Math.PI,m=t=>t>0?1:-1,P=l*=500*k/t**2,T=m(p)*k/4,E=i*=(1+2*s*Math.random()-s)*k/t,I=[],R=0,C=0,A=0,V=1,q=0,L=0,B=0;for(h*=500*k/t**3,p*=k/t,f*=k/t,d*=t,u=u*t|0,S=(n=99+n*t)+(w*=t)+(r*=t)+(a*=t)+(y*=t)|0;A1?o>2?o>3?Math.sin((R%k)**3):Math.max(Math.min(Math.tan(R),1),-1):1-(2*R/k%2+2)%2:1-4*Math.abs(Math.round(R/k)-R/k):Math.sin(R),B=(u?1-b+b*Math.sin(2*Math.PI*A/u):1)*m(B)*Math.abs(B)**c*e*.5*(AA?0:(Ad&&(i+=f,E+=f,V=0),!u||++q%u||(i=E,l=P,V=V||1);return I};function s(t,e){const s=t.audioContext||new AudioContext,i=t.sampleRate||s.sampleRate,n=t.channels||2,r=1.5*t.decay,a=Math.ceil(i*r),o=Math.ceil(i*t.fadeIn),c=Math.ceil(i*t.decay),l=Math.pow(.001,1/c),h=s.createBuffer(n,a,i);for(let t=0;t51?(t-8)%44+8:t){case 0:c(57,t+0,t+.25),c(45,t+0,t+2),c(60,t+.25,t+.5),c(57,t+.5,t+.75),c(64,t+.75,t+1.25);break;case 1:c(60,t+.25,t+.75),c(60,t+.75,t+1);break;case 2:c(52,t+0,t+1),c(60,t+.25,t+.5),c(57,t+.5,t+.75),c(64,t+.75,t+1.25);break;case 3:c(52,t+0,t+.75),c(60,t+.25,t+.75),c(60,t+.75,t+1),c(52,t+.75,t+1);break;case 4:c(45,t+0,t+1.75),c(57,t+.25,t+.5),c(60,t+.5,t+.75),c(64,t+.75,t+1.25),c(57,t+.75,t+1.25);break;case 5:c(57,t+.25,t+.5),c(60,t+.5,t+1),c(52,t+.75,t+1);break;case 6:c(60,t+0,t+1),c(52,t+0,t+.5),c(52,t+.5,t+1.5);break;case 7:c(64,t+0,t+1),c(52,t+.5,t+.75),c(52,t+.75,t+1);break;case 8:c(57,t+0,t+.25),c(45,t+0,t+.75),c(57,t+.25,t+.5),c(57,t+.5,t+.75),c(60,t+.75,t+1.25),c(52,t+.75,t+1.75);break;case 9:c(69,t+.25,t+.5),c(67,t+.5,t+.75),c(69,t+.75,t+1),c(52,t+.75,t+1);break;case 10:c(69,t+0,t+.75),c(41,t+0,t+.25),c(48,t+.25,t+.5),c(55,t+.5,t+1.5),c(57,t+.75,t+1.75);break;case 11:c(55,t+.5,t+.75),c(67,t+.75,t+1),c(50,t+.75,t+1);break;case 12:c(67,t+0,t+.25),c(55,t+0,t+.75),c(67,t+.25,t+.5),c(67,t+.5,t+.75),c(66,t+.75,t+1.25),c(57,t+.75,t+1.75),c(54,t+.75,t+1.75);break;case 13:c(67,t+.25,t+.5),c(66,t+.5,t+.75),c(64,t+.75,t+1),c(47,t+.75,t+1);break;case 14:c(52,t+0,t+.25),c(47,t+0,t+2),c(54,t+.25,t+.5),c(56,t+.5,t+.75),c(59,t+.75,t+1.75);break;case 15:c(57,t+.75,t+1);break;case 16:c(57,t+0,t+.25),c(45,t+0,t+.75),c(57,t+.25,t+.5),c(57,t+.5,t+.75),c(60,t+.75,t+1.25),c(52,t+.75,t+1.25);break;case 17:c(69,t+.25,t+.5),c(57,t+.25,t+1),c(67,t+.5,t+.75),c(69,t+.75,t+1);break;case 18:c(69,t+0,t+.75),c(57,t+0,t+.25),c(53,t+0,t+.25),c(48,t+0,t+.25),c(48,t+.25,t+.5),c(55,t+.5,t+.75),c(65,t+.75,t+1.75),c(57,t+.75,t+1.25);break;case 19:c(57,t+.25,t+.5),c(60,t+.5,t+1),c(67,t+.75,t+1);break;case 20:c(67,t+0,t+.25),c(43,t+0,t+.5),c(59,t+.25,t+.5),c(67,t+.5,t+.75),c(50,t+.5,t+.75),c(66,t+.75,t+1.25),c(47,t+.75,t+1.75);break;case 21:c(67,t+.25,t+.5),c(66,t+.5,t+.75),c(64,t+.75,t+1),c(47,t+.75,t+1);break;case 22:c(52,t+0,t+.25),c(47,t+0,t+2),c(54,t+.25,t+.5),c(56,t+.5,t+.75),c(59,t+.75,t+1.75);break;case 23:c(67,t+.75,t+1);break;case 24:c(67,t+0,t+.25),c(59,t+0,t+.5),c(55,t+0,t+.5),c(62,t+.25,t+.5),c(67,t+.5,t+.75),c(59,t+.5,t+.75),c(67,t+.75,t+1.25),c(57,t+.75,t+1.25);break;case 25:c(67,t+.25,t+.5),c(50,t+.25,t+.5),c(67,t+.5,t+.75),c(43,t+.5,t+1),c(68,t+.75,t+1),c(64,t+.75,t+1);break;case 26:c(68,t+0,t+.25),c(64,t+0,t+.25),c(40,t+0,t+.5),c(64,t+.25,t+.5),c(62,t+.5,t+.75),c(47,t+.5,t+.75),c(62,t+.75,t+1.25),c(52,t+.75,t+1.5);break;case 27:c(60,t+.25,t+.75),c(52,t+.5,t+.75),c(57,t+.75,t+1),c(47,t+.75,t+1);break;case 28:c(60,t+0,t+.5),c(45,t+0,t+.5),c(72,t+.5,t+1),c(64,t+.5,t+1),c(57,t+.5,t+.75),c(48,t+.75,t+1);break;case 29:c(60,t+0,t+.25),c(57,t+0,t+.75),c(53,t+0,t+.75),c(65,t+.25,t+.5),c(67,t+.5,t+.75),c(72,t+.75,t+1),c(43,t+.75,t+1);break;case 30:c(72,t+0,t+.25),c(64,t+0,t+.25),c(60,t+0,t+.25),c(48,t+0,t+.5),c(36,t+0,t+.5),c(72,t+.25,t+.5),c(64,t+.25,t+.5),c(60,t+.25,t+.5),c(72,t+.5,t+.75),c(64,t+.5,t+.75),c(60,t+.5,t+.75),c(55,t+.5,t+.75),c(72,t+.75,t+1.25),c(64,t+.75,t+1.25),c(60,t+.75,t+1.25),c(50,t+.75,t+1.5);break;case 31:c(71,t+.25,t+1),c(62,t+.25,t+1),c(59,t+.25,t+1),c(57,t+.5,t+.75),c(50,t+.75,t+1);break;case 32:c(72,t+0,t+.25),c(64,t+0,t+.25),c(60,t+0,t+.25),c(57,t+0,t+.5),c(52,t+0,t+.5),c(45,t+0,t+.5),c(72,t+.25,t+.5),c(72,t+.5,t+.75),c(59,t+.5,t+1),c(72,t+.75,t+1.25);break;case 33:c(57,t+0,t+.5),c(53,t+0,t+.5),c(48,t+0,t+.5),c(71,t+.25,t+.5),c(69,t+.5,t+.75),c(48,t+.5,t+1),c(67,t+.75,t+1),c(64,t+.75,t+1),c(60,t+.75,t+1);break;case 34:c(67,t+0,t+.25),c(64,t+0,t+.25),c(60,t+0,t+.25),c(36,t+0,t+.5),c(69,t+.25,t+.5),c(71,t+.5,t+.75),c(43,t+.5,t+.75),c(72,t+.75,t+1.25),c(68,t+.75,t+1.25),c(64,t+.75,t+1.25),c(48,t+.75,t+1);break;case 35:c(40,t+0,t+.5),c(71,t+.25,t+1),c(56,t+.5,t+.75),c(52,t+.75,t+1);break;case 36:c(60,t+0,t+.5),c(45,t+0,t+.25),c(52,t+.25,t+.5),c(72,t+.5,t+1),c(64,t+.5,t+1),c(60,t+.5,t+1),c(57,t+.5,t+1);break;case 37:c(60,t+0,t+.25),c(57,t+0,t+.5),c(53,t+0,t+.5),c(65,t+.25,t+.5),c(67,t+.5,t+.75),c(48,t+.5,t+.75),c(72,t+.75,t+1),c(60,t+.75,t+1),c(43,t+.75,t+1);break;case 38:c(72,t+0,t+.25),c(67,t+0,t+.25),c(60,t+0,t+.25),c(48,t+0,t+.5),c(72,t+.25,t+.5),c(60,t+.25,t+.5),c(72,t+.5,t+.75),c(60,t+.5,t+.75),c(55,t+.5,t+.75),c(72,t+.75,t+1.25),c(67,t+.75,t+1.25),c(62,t+.75,t+1.25),c(50,t+.75,t+1);break;case 39:c(43,t+0,t+.25),c(71,t+.25,t+.75),c(67,t+.25,t+.75),c(62,t+.25,t+.75),c(50,t+.25,t+.5),c(57,t+.5,t+1),c(59,t+.75,t+1);break;case 40:c(72,t+0,t+.5),c(64,t+0,t+.5),c(60,t+0,t+.5),c(52,t+0,t+.5),c(45,t+0,t+.5),c(72,t+.5,t+.75),c(64,t+.5,t+.75),c(60,t+.5,t+.75),c(57,t+.5,t+.75),c(72,t+.75,t+1.25),c(67,t+.75,t+1.25),c(60,t+.75,t+1.25),c(48,t+.75,t+1);break;case 41:c(57,t+0,t+.5),c(53,t+0,t+.5),c(48,t+0,t+.5),c(71,t+.25,t+.5),c(69,t+.5,t+.75),c(60,t+.5,t+.75),c(57,t+.5,t+.75),c(67,t+.75,t+1),c(53,t+.75,t+1);break;case 42:c(67,t+0,t+.25),c(48,t+0,t+.25),c(36,t+0,t+.25),c(69,t+.25,t+.5),c(43,t+.25,t+.5),c(71,t+.5,t+.75),c(48,t+.5,t+.75),c(72,t+.75,t+1.25),c(68,t+.75,t+1.25),c(64,t+.75,t+1.25),c(60,t+.75,t+1.25),c(44,t+.75,t+1);break;case 43:c(52,t+0,t+.25),c(71,t+.25,t+1),c(68,t+.25,t+1),c(64,t+.25,t+1),c(47,t+.25,t+.5),c(56,t+.5,t+.75),c(52,t+.75,t+1);break;case 44:c(72,t+0,t+.5),c(64,t+0,t+.5),c(60,t+0,t+.5),c(57,t+0,t+.25),c(45,t+0,t+.25),c(52,t+.25,t+.5),c(72,t+.5,t+.75),c(60,t+.5,t+.75),c(57,t+.5,t+.75),c(72,t+.75,t+1.25),c(67,t+.75,t+1.25),c(60,t+.75,t+1.25),c(48,t+.75,t+1);break;case 45:c(57,t+0,t+.25),c(53,t+0,t+.25),c(71,t+.25,t+.5),c(55,t+.25,t+.5),c(69,t+.5,t+.75),c(48,t+.5,t+1),c(67,t+.75,t+1),c(64,t+.75,t+1),c(60,t+.75,t+1);break;case 46:c(67,t+0,t+.25),c(64,t+0,t+.25),c(60,t+0,t+.25),c(48,t+0,t+.25),c(36,t+0,t+.25),c(69,t+.25,t+.5),c(43,t+.25,t+.5),c(71,t+.5,t+.75),c(48,t+.5,t+1),c(72,t+.75,t+1.25),c(68,t+.75,t+1.25),c(60,t+.75,t+1.25);break;case 47:c(52,t+0,t+.5),c(40,t+0,t+.5),c(71,t+.25,t+1),c(68,t+.25,t+1),c(62,t+.25,t+1),c(47,t+.5,t+.75),c(52,t+.75,t+1);break;case 48:c(71,t+0,t+2),c(68,t+0,t+2),c(62,t+0,t+2),c(52,t+0,t+.25),c(54,t+.25,t+.75),c(56,t+.75,t+1.25);break;case 49:c(54,t+.25,t+.5),c(56,t+.5,t+1);break;case 50:c(64,t+0,t+1.75);break;case 51:c(52,t+.75,t+1)}}let h=-1;function f(){let t=n.currentTime-a+4,e=(h+1)*i;if(!(e>t))for(t+=4;e=540&&(s.x-=(s.x-i.x)*e,s.y=539),s.x<0?s.x=0:s.x>=960&&(s.x=959)}interpolate(t){this.interpolated.set(v(this.oldPosition.x,this.position.x,t),v(this.oldPosition.y,this.position.y,t))}}class q extends V{constructor(t,e,s){super(t,e,s),this.x=e,this.y=s}set(t,e){this.x=t,this.y=e}integrate(){this.position.set(this.x,this.y),this.oldPosition.set(this.x,this.y)}interpolate(t){this.interpolated.set(this.x,this.y)}}class L{constructor(t,e,s,i=!1,n=1){if(this.parent=t,this.v0=e,this.v1=s,this.p0=e.position,this.p1=s.position,this.dist=this.p0.distanceSquared(this.p1),this.edge=i,this.stiffness=n,!this.dist)throw Error("Overlapping vertices.");t.constraints.push(this),i&&t.edges.push(this),t.scene.constraints.push(this)}solve(){m.setSubtract(this.p0,this.p1);const t=this.dist/(m.dot(m)+this.dist)-.5;m.scalarMult(t*this.stiffness),this.p0.add(m),this.p1.subtract(m)}}class B{constructor(t,e=1){this.scene=t,this.vertices=[],this.positions=[],this.constraints=[],this.edges=[],this.center=new u,this.halfExtents=new u,this.pMin=0,this.pMax=0,this.mass=e,t.bodies.push(this)}boundingBox(){let t=this.positions[0],e=t.x,s=t.x,i=t.y,n=t.y;for(let r=1;rs&&(s=t.x),t.yn&&(n=t.y);this.center.set(.5*(e+s),.5*(i+n)),this.halfExtents.set(.5*(s-e),.5*(n-i))}projectOn(t){let e=this.positions[0].dot(t);this.pMin=this.pMax=e;for(let s=1;sthis.pMax&&(this.pMax=e)}paint(t,e){for(const t of this.vertices)t.interpolate(e);t.beginPath();for(const e of this.constraints)e.edge||(t.moveTo(e.v0.interpolated.x,e.v0.interpolated.y),t.lineTo(e.v1.interpolated.x,e.v1.interpolated.y));t.strokeStyle="#ffac7f",t.stroke(),t.beginPath();for(const e of this.vertices)t.lineTo(e.interpolated.x,e.interpolated.y);t.closePath(),t.strokeStyle="#99c2db",t.stroke()}}const D=function(){const t=new u;let e,s,i;function n(t,e,s){return m.setNormal(s.p0,s.p1),t.projectOn(m),e.projectOn(m),t.pMin=r.halfExtents.x+a.halfExtents.x||Math.abs(a.center.y-r.center.y)>=r.halfExtents.y+a.halfExtents.y)return;e=n(r,a,s=r.edges[0]),t.setTo(m);for(let i=1;i0)return;c>e&&(e=c,s=o,t.setTo(m))}for(let i=0;i0)return;c>e&&(e=c,s=o,t.setTo(m))}if(s.parent!==a){const t=r;r=a,a=t}m.setSubtract(r.center,a.center),m.dot(t)<0&&t.scalarMult(-1),m.setSubtract(r.positions[0],a.center);let l=t.dot(m);i=r.vertices[0];for(let e=1;eMath.abs(P.y)?(a.x-m.x-n.x)/P.x:(a.y-m.y-n.y)/P.y;let c=i.parent.mass,l=s.parent.mass;const h=c+l;c/=2*h,l/=h;const f=c/(o*o+(1-o)*(1-o)),d=(1-o)*f,u=o*f;n.x-=m.x*d,n.y-=m.y*d,r.x-=m.x*u,r.y-=m.y*u,a.x+=m.x*l,a.y+=m.y*l,0}()}}();class O extends B{constructor(t,e,s,i,n=9,r=.5,a=1){super(t,a);const o=x/n;for(let t=0;t1.44?(t.height=2*i,t.width=2*s,e.scale(2,2)):(t.height=i,t.width=s)}const G=(F=".can",document.querySelector(F));var F;const W=G.getContext("2d");j(G,W,960,540);const U=16/9;let N=1,z="transform";z in G.style||(z="webkitTransform");const $="undefined"!=typeof visualViewport;function H(){let t=$?visualViewport.width:innerWidth,e=$?visualViewport.height:innerHeight;t/e>U?t=e*U:e=t/U,N=960/t;const s=t/960;G.style[z]=`scale3d(${s},${s},1)`}addEventListener("resize",H),addEventListener("orientationchange",H),$&&visualViewport.addEventListener("resize",H);const Q="16px -apple-system, 'Segoe UI', system-ui, Roboto, sans-serif",X=Q.replace("16","bold 48");G.addEventListener("contextmenu",t=>{t.preventDefault()});const Y={dragging:!1,x:0,y:0};function _(t){const e=G.getBoundingClientRect();Y.x=(t.clientX-e.left)*N,Y.y=(t.clientY-e.top)*N}document.addEventListener("mousedown",t=>{t.preventDefault(),Y.dragging=!0,_(t)}),document.addEventListener("mousemove",t=>{t.preventDefault(),_(t)}),document.addEventListener("mouseup",t=>{t.preventDefault(),Y.dragging=!1,Y.vertex=void 0,C||A()}),document.addEventListener("touchstart",t=>{t.preventDefault(),Y.dragging=!0,_(t.targetTouches[0])}),document.addEventListener("touchmove",t=>{t.preventDefault(),_(t.targetTouches[0])}),document.addEventListener("touchend",t=>{t.preventDefault(),Y.dragging=!1,Y.vertex=void 0,C||A()}),document.addEventListener("touchcancel",t=>{t.preventDefault(),Y.dragging=!1,Y.vertex=void 0});const J=function(){let t=t=>{},e=t=>{};const s=.02;let i=-1,n=0;function r(a){requestAnimationFrame(r),-1===i&&(i=a),n+=.001*(a-i),i=a;let o=2;for(;n>0;)n-=s,o>0&&(t(s),--o);e(n/s+1)}return function(s,i){t=s,e=i,requestAnimationFrame(r)}}();class K extends B{constructor(t,e){super(t),this.startingVertex=new q(this,e.x,e.y),this.targetingVertex=new V(this,e.x-.001,e.y),this.lastPosition=new u(e.x,e.y);const s=new L(this,this.startingVertex,this.targetingVertex,!1,.1),i=s.solve;s.solve=()=>{Y.vertex||(i.call(s),this.startingVertex.position.setTo(e))}}paint(t,e){this.targetingVertex.interpolate(e);const s=this.startingVertex.position,i=this.targetingVertex.interpolated;t.beginPath(),t.moveTo(i.x,i.y),t.lineTo(s.x,s.y),t.strokeStyle=Pt,t.stroke(),t.beginPath(),t.arc(i.x,i.y,9,0,x),t.arc(s.x,s.y,4,0,x),t.fillStyle=Pt,t.fill()}}class Z extends B{constructor(t,e,s,i,n=0,r=1,a=9){super(t,a);const o=p;for(let t=0;t<4;++t){const r=o*t+n;new V(this,e+i*Math.cos(r),s+i*Math.sin(r))}for(let t=0;tn&&(n=s.x),s.ya&&(a=s.y);this.center.set(.5*(i+n),.5*(r+a)),this.halfExtents.set(.5*(n-i),.5*(a-r));for(let t=0;t<16;++t)this.relInterp[t].setSubtract(this.vertices[t].interpolated,this.center)}tracePath(t,e){t.beginPath();for(const[s,i,n,r]of e)m.setScalarMult(this.relInterp[s],n),P.setScalarMult(this.relInterp[i],r),m.add(P),m.add(this.center),t.lineTo(m.x,m.y);t.closePath()}}const et=[[0,0,1,0],[1,1,1,0],[2,2,1,0],[3,3,1,0],[4,4,1,0],[5,5,1,0],[6,6,1,0],[7,7,1,0],[8,8,1,0],[9,9,1,0],[10,9,.56,.44],[10,11,.75,.21],[10,9,.59,.18],[11,10,.6,.16],[10,11,.39,.16],[12,11,.2,0],[8,7,.25,.22],[7,8,.46,.01],[6,7,.46,.01],[5,6,.46,.01],[4,3,.46,0],[3,4,.46,0],[2,1,.45,0],[1,0,.42,.01],[0,15,.4,.01],[15,14,.39,.01],[14,13,.37,.01],[13,14,.21,.17],[14,13,.43,.01],[15,14,.35,.17],[14,13,.51,.01],[13,12,.6,.01],[13,12,.52,.34],[13,12,.81,.31],[13,12,1,.01],[14,15,.65,.25],[15,0,.85,.04],[15,14,.54,.4],[14,15,.78,.19],[14,15,.52,.48],[15,15,1,0]],st=W.createRadialGradient(256,576,256,256,-192,256);st.addColorStop(1-.7,"#e31587"),st.addColorStop(.47,"#ff3647"),st.addColorStop(.63,"#ff980e"),st.addColorStop(.95,"#fff44f");const it=W.createLinearGradient(0,540,960,0);it.addColorStop(0,"#4facfe"),it.addColorStop(1,"#00f2fe");class nt extends tt{paint(t,e){this.interpolate(e),t.beginPath();for(const e of this.vertices)t.lineTo(e.interpolated.x,e.interpolated.y);t.closePath(),t.fillStyle=it,t.fill(),this.tracePath(t,et),t.save(),S(this.center.x-this.halfExtents.x,this.center.y-this.halfExtents.y,this.center.x+this.halfExtents.x,this.center.y+this.halfExtents.y),t.fillStyle=st,t.fill(),t.restore()}}const rt=[[1,1,1,0],[2,2,1,0],[3,3,1,0],[4,4,1,0],[5,5,1,0],[6,6,1,0],[7,7,1,0],[8,8,1,0],[9,9,1,0],[10,10,1,0],[11,11,1,0],[12,12,1,0],[13,13,1,0],[14,14,1,0],[15,15,1,0],[0,0,1,0],[0,1,.68,.33],[8,7,.48,.33],[8,9,.43,.38],[15,0,.38,.08],[15,14,.46,0],[14,13,.51,.01],[13,12,.54,.01],[12,11,.55,.01],[11,12,.54,.01],[10,11,.51,0],[9,8,.47,0],[9,8,.38,.09],[7,8,.33,.14],[7,8,.46,.01],[6,7,.51,.01],[5,6,.55,0],[4,5,.54,.01],[3,4,.54,.01],[2,1,.52,0],[2,1,.41,.11],[1,0,.86,.14]],at=W.createRadialGradient(256,256,363,256,128,0);at.addColorStop(0,"#0d79c8"),at.addColorStop(.7376,"#86e8fd"),at.addColorStop(1,"#89eafe");class ot extends tt{paint(t,e){this.interpolate(e),this.tracePath(t,rt),t.save(),S(this.center.x-this.halfExtents.x,this.center.y-this.halfExtents.y,this.center.x+this.halfExtents.x,this.center.y+this.halfExtents.y),t.fillStyle=at,t.fill(),t.restore()}}const ct=[[0,0,1,0],[1,1,1,0],[2,2,1,0],[3,3,1,0],[4,4,1,0],[5,5,1,0],[1,2,.31,.15],[12,11,.44,.01],[14,14,1,0],[15,15,1,0]],lt=[[5,5,1,0],[6,6,1,0],[7,7,1,0],[8,8,1,0],[9,9,1,0],[10,10,1,0],[7,6,.31,.15],[1,2,.31,.15],[4,5,.88,.12]],ht=[[10,10,1,0],[11,11,1,0],[12,12,1,0],[13,13,1,0],[14,14,1,0],[15,14,.78,.22],[12,11,.44,.01],[7,6,.31,.15],[9,10,.56,.44]],ft=[[0,1,.45,0],[1,0,.44,.01],[2,3,.44,.01],[3,4,.44,.01],[4,3,.45,0],[5,6,.44,.01],[6,7,.45,.01],[7,8,.44,.01],[8,9,.46,0],[9,10,.45,.01],[10,11,.45,.01],[11,10,.45,0],[12,11,.44,.01],[13,12,.44,.01],[14,13,.44,.01],[15,14,.44,.01]];class dt extends tt{paint(t,e){this.interpolate(e),this.tracePath(t,ct),t.fillStyle="#ffcd40",t.fill(),this.tracePath(t,lt),t.fillStyle="#0f9d58",t.fill(),this.tracePath(t,ht),t.fillStyle="#db4437",t.fill(),this.tracePath(t,ft),t.fillStyle="#4285f4",t.fill(),t.lineWidth=2.5,t.strokeStyle="#f1f1f1",t.stroke(),t.lineWidth=1}}class ut extends B{constructor(t,e,s,i=1,n=9){super(t,n);const r=new q(this,e,s),a=new q(this,e+64,s),o=new q(this,e+64,s+256),c=new q(this,e,s+256);new L(this,r,a,!0,i),new L(this,a,o,!0,i),new L(this,o,c,!0,i),new L(this,c,r,!0,i),new L(this,r,o,!1,i),new L(this,a,c,!1,i),this.center.set(e+32,s+128),this.halfExtents.set(32,128)}rotate(t){const e=Math.cos(t),s=Math.sin(t);for(const t of this.vertices)m.setSubtract(t.position,this.center),t.set(this.center.x+m.x*e-m.y*s,this.center.y+m.x*s+m.y*e),t.integrate()}paint(t,e){for(const t of this.vertices)t.interpolate(e);t.beginPath();for(let e=0;e<4;++e)t.lineTo(this.vertices[e].interpolated.x,this.vertices[e].interpolated.y);t.closePath(),t.save(),t.clip(),t.drawImage(Rt,0,0,960,540),t.restore(),t.strokeStyle=Pt,t.stroke()}}class xt extends B{constructor(t,e,s,i,n=.5,r=1){super(t,r),this.center.set(e,s),this.halfExtents.set(.5*i,.5*i);const a=p,o=this.halfExtents.length();for(let t=0;t<4;++t){const i=a*t+g;new V(this,e+o*Math.cos(i),s+o*Math.sin(i))}for(let t=0;t{t.fillStyle="#f1f1f1",t.fillRect(0,0,88,64),t.save(),t.translate(1,0),t.scale(5,5),t.fillStyle=gt,t.fillRect(0,0,3,3),t.fillStyle=yt,t.fillRect(4,1,2,2),t.fillStyle="#fbbc05",t.fillRect(7,1,2,2),t.fillStyle=gt,t.fillRect(10,1,2,3),t.fillStyle="#34a853",t.fillRect(13,0,1,3),t.fillStyle=yt,t.fillRect(15,1,2,2),t.restore(),t.save(),t.translate(0,30),t.scale(3,3),t.fillStyle="#ddd",t.fillRect(0,0,29,4),t.fillStyle="#cdcdcd",t.fillRect(3,7,10,4),t.fillRect(16,7,10,4),t.restore()});class wt{constructor(){this.x=850,this.y=172,this.width=110,this.height=196}update(){}contains(t){return t.x>=this.x&&t.x-this.x=this.y&&t.y-this.y80))if(++this.n,this.old.x=this.x,this.old.y=this.y,this.old.width=this.width,this.old.height=this.height,this.n<=25){const t=.04*this.n;this.x=v(850,807,w(t)),this.y=v(172,387,w(t)),this.width=v(110,153,w(t)),this.height=v(196,153,w(t))}else{const t=(this.n-25)/55;this.x=v(807,252,b(t)),this.y=v(387,430,b(t)),this.width=v(153,196,b(t)),this.height=v(153,110,b(t))}}paint(t,e){let s,i,n,r;this.n>80?(s=this.x,i=this.y,n=this.width,r=this.height):(s=v(this.old.x,this.x,e),i=v(this.old.y,this.y,e),n=v(this.old.width,this.width,e),r=v(this.old.height,this.height,e)),t.fillStyle="#f1f1f1",t.fillRect(s,i,n,r),t.drawImage(vt,s+.5*(n-88),i+.5*(r-64),88,64)}}class Mt extends wt{contains(){return!1}paint(){}}class St extends class{constructor(){this.vertices=[],this.constraints=[],this.bodies=[]}integrate(){for(let t=0;t{t.fillStyle=Pt,t.fillRect(0,0,960,540),Tt(t,"404 Not Found")}),It=k(960,540,t=>{t.fillStyle=Pt,t.fillRect(0,0,960,540),Tt(t,"301 Moved")}),Rt=k(960,540,t=>{t.beginPath();for(let e=0;e<1500;e+=20)t.moveTo(e,0),t.lineTo(e-540,540);t.strokeStyle=Pt,t.stroke()});function Ct(t,e,s){t.clearRect(0,0,960,540),s.constructor===St?(t.font=Q,t.textAlign="center",t.textBaseline="top",t.fillStyle="#f1f1f1",t.fillText("1. Pull",s.startingPoint.x,s.startingPoint.y+48),t.fillText("2. Release",s.startingPoint.x-256,s.startingPoint.y+48)):s.constructor===mt&&(t.font=Q,t.textAlign="center",t.textBaseline="middle",t.fillStyle="#f1f1f1",t.fillText("Written by Mark Vasilkov for js13kGames in 2020",480,516),t.font=X,t.fillStyle=it,t.fillText("Thank you for playing!",480,135))}function At(t,e,s,i,n,r=0){t.save(),t.translate(s,i),t.rotate(-.5124),t.beginPath(),t.lineTo(r,-500),t.lineTo(n,-500),t.lineTo(n,1e3),t.lineTo(r,1e3),t.closePath(),t.restore(),t.save(),t.clip(),t.drawImage(e,0,0,960,540),t.restore()}const Vt=[St,kt,class extends St{constructor(t,e=0){super(t,e),new ut(this,t.x+256,2,1,9999).rotate(y),new ut(this,t.x+256,282,1,9999).rotate(-y)}getIndex(){return 2}},class extends kt{constructor(t,e=0){super(t,e);for(let e=0;e<8;++e)(e<2||e>5)&&new xt(this,t.x+256+32,14+64*e+32,64,.5,.5);new ut(this,t.x+256,-242,1,9999),new ut(this,t.x+256,526,1,9999)}getIndex(){return 3}},class extends kt{constructor(t,e=0){super(t,e),this.duration=196}updateTargeting(t){if(this.reticle.lastPosition.setTo(t),m.setSubtract(this.startingPoint,t),m.length()<16)return;const e=Math.atan2(m.y,m.x),s=64*Math.cos(e),i=64*Math.sin(e);m.scalarMult(256/m.length()),m.add(this.startingPoint);const n=this.wall.vertices;let r=m.x+128*Math.cos(e-p),a=m.y+128*Math.sin(e-p);n[0].set(r,a),n[1].set(r+s,a+i),r=m.x+128*Math.cos(e+p),a=m.y+128*Math.sin(e+p),n[2].set(r+s,a+i),n[3].set(r,a)}getIndex(){return 4}},class extends St{constructor(t,e=0){super(t,e),m.set(.5*(850-t.x),-67.5);for(const t of this.projectile.vertices)t.position.add(m),t.oldPosition.add(m);this.projectile.center.add(m),new xt(this,t.x,t.y,64,.5,4)}getIndex(){return 5}},class extends St{constructor(t,e=0){super(t,e),this.website=new bt,this.curtainPicture=It}solve(){super.solve(),3!==this.state&&4!==this.state&&6!==this.state||this.website.update()}getIndex(){return 6}},class extends St{constructor(t,e=0){super(t,e),this.clone=new ot(this,905,270)}integrate(){do{if(3!==this.state)break;m.setSubtract(this.projectile.center,this.clone.center);const t=m.length();if(t<64)break;m.scalarMult(1/t);for(const t of this.clone.vertices)t.position.add(m)}while(0);super.integrate()}getIndex(){return 7}},class extends St{static getUserAgent(){return ot}constructor(t,e=0){super(t,e),this.website=new Mt,this.duration=196,this.autoWin=!0,new pt(this,849,172)}integrate(){let t,e;3===this.state||4===this.state||6===this.state?(t=.9,e=.5):(t=0,e=0);for(let s=0;s=qt.duration?qt.autoWin?(qt.state=6,Lt=new(Vt[(qt.getIndex()+1)%Vt.length])(t),i=0,Y.dragging=!1,Y.vertex=void 0,T(R)):(qt.state=4,T(I)):qt.website.contains(qt.projectile.center)&&--s<=0&&(qt.state=6,Lt=new(Vt[(qt.getIndex()+1)%Vt.length])(t),i=0,Y.dragging=!1,Y.vertex=void 0,T(R)):4===qt.state?++qt.curtain>=24&&(qt=new(Vt[qt.getIndex()])(t,24),qt.state=5,Y.dragging=!1,Y.vertex=void 0):5===qt.state?--qt.curtain<=0&&(qt.state=0):6===qt.state&&++i>69&&(qt=Lt)}),(function(s){let n,r;if(6===qt.state)W.fillStyle=it,W.fillRect(0,0,960,540),W.save(),n=(i-1+s)/69,r=v(1,.5,M(n)),W.translate(480,270),W.scale(r,r),W.translate(-480,-270),W.translate(v(0,-960,M(n)),0),W.beginPath(),W.rect(0,0,960,540),W.clip();else if(Y.dragging&&5!==qt.state){if(!Y.vertex&&t.distanceSquared(Y)<=4096&&(Y.vertex=qt.reticle.targetingVertex,0===qt.state&&(qt.state=1)),Y.vertex){const e=qt.reticle.targetingVertex.position;e.setTo(Y);const s=t.distanceSquared(e);s>65536&&(e.subtract(t),e.scalarMult(256/Math.sqrt(s)),e.add(t)),qt.updateTargeting(e)}}else 1===qt.state&&(qt.launch()?(qt.state=2,e=4,T(E)):qt.state=0);Ct(W,0,qt),qt.website.paint(W,s);for(const t of qt.bodies)t.paint(W,s);if(function(t,e,s){let i;3===s.state?(i=(s.waited-1+e)/s.duration*960,t.fillStyle=Pt,t.fillRect(0,0,i,3)):4===s.state?(i=.5*b((s.curtain-1+e)/24)*1102,t.fillStyle=Pt,t.fillRect(0,0,960,3),At(t,s.curtainPicture,0,540,i),At(t,s.curtainPicture,960,0,-i)):5===s.state&&(i=.5*w((s.curtain+1-e)/24)*1102,At(t,s.curtainPicture,480,270,-i,i))}(W,s,qt),6===qt.state){W.restore(),W.save(),W.translate(v(960,0,M(n)),0),Ct(W,0,Lt),Lt.website.paint(W,s);for(const t of Lt.bodies)t.paint(W,s);W.restore()}}))}()}(); -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | I want to google the game
-------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"I want to google the game","short_name":"js13k-2020","display":"standalone","orientation":"landscape"} -------------------------------------------------------------------------------- /out/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "clean-css-cli": "^4.3.0", 4 | "html-minifier": "^4.0.0", 5 | "terser": "^5.3.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "None", 5 | "declaration": false, 6 | "outDir": "./out", 7 | "rootDir": "./typescript", 8 | "strict": true, 9 | "noUnusedLocals": true, 10 | "noImplicitReturns": true, 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "newLine": "LF" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typescript/Background.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const FAILURE_BACK = canvas.createLinearGradient(0, 0, Settings.screenWidth, 0) 5 | // Colors: https://uigradients.com/#DayTripper 6 | FAILURE_BACK.addColorStop(0, '#f857a6') 7 | FAILURE_BACK.addColorStop(1, '#ff5858') 8 | 9 | function walloftext(canvas: CanvasRenderingContext2D, text: string) { 10 | canvas.font = systemFont 11 | canvas.textAlign = 'center' 12 | canvas.textBaseline = 'top' 13 | canvas.fillStyle = '#fff' 14 | 15 | for (let x = 80; x < Settings.screenWidth; x += 160) { 16 | for (let y = 15; y < Settings.screenHeight; y += 45) { 17 | canvas.fillText(text, x, y) 18 | } 19 | } 20 | } 21 | 22 | const FAILURE_PICTURE = prerender(Settings.screenWidth, Settings.screenHeight, canvas => { 23 | canvas.fillStyle = FAILURE_BACK 24 | canvas.fillRect(0, 0, Settings.screenWidth, Settings.screenHeight) 25 | 26 | walloftext(canvas, '404 Not Found') 27 | }) 28 | 29 | const FAILURE_MOVED_PICTURE = prerender(Settings.screenWidth, Settings.screenHeight, canvas => { 30 | canvas.fillStyle = FAILURE_BACK 31 | canvas.fillRect(0, 0, Settings.screenWidth, Settings.screenHeight) 32 | 33 | walloftext(canvas, '301 Moved') 34 | }) 35 | 36 | const WALL_PICTURE = prerender(Settings.screenWidth, Settings.screenHeight, canvas => { 37 | canvas.beginPath() 38 | 39 | for (let x = 0; x < Settings.screenWidth + Settings.screenHeight; x += 20) { 40 | canvas.moveTo(x, 0) 41 | canvas.lineTo(x - Settings.screenHeight, Settings.screenHeight) 42 | } 43 | 44 | canvas.strokeStyle = FAILURE_BACK 45 | canvas.stroke() 46 | }) 47 | 48 | function paintBackground(canvas: CanvasRenderingContext2D, t: number, level: Level) { 49 | canvas.clearRect(0, 0, Settings.screenWidth, Settings.screenHeight) 50 | 51 | if (level.constructor === Level) { 52 | canvas.font = systemFont 53 | canvas.textAlign = 'center' 54 | canvas.textBaseline = 'top' 55 | canvas.fillStyle = '#f1f1f1' 56 | 57 | canvas.fillText('1. Pull', level.startingPoint.x, level.startingPoint.y + 48) 58 | canvas.fillText('2. Release', level.startingPoint.x - Settings.targetReleaseDist, 59 | level.startingPoint.y + 48) 60 | } 61 | else if (level.constructor === End) { 62 | canvas.font = systemFont 63 | canvas.textAlign = 'center' 64 | canvas.textBaseline = 'middle' 65 | canvas.fillStyle = '#f1f1f1' 66 | 67 | canvas.fillText('Written by Mark Vasilkov for js13kGames in 2020', Settings.screenWidth * 0.5, Settings.screenHeight - 24) 68 | 69 | canvas.font = systemFontHeading 70 | canvas.fillStyle = EARTH_BACK 71 | 72 | canvas.fillText('Thank you for playing!', Settings.screenWidth * 0.5, Settings.screenHeight * 0.25) 73 | } 74 | } 75 | 76 | function paintCurtain(canvas: CanvasRenderingContext2D, t: number, level: Level) { 77 | let width: number 78 | 79 | if (level.state === LevelState.WAITING) { 80 | width = (level.waited - 1 + t) / level.duration * Settings.screenWidth 81 | 82 | canvas.fillStyle = FAILURE_BACK 83 | canvas.fillRect(0, 0, width, 3) 84 | } 85 | 86 | else if (level.state === LevelState.FAILING) { 87 | width = easeOutQuad((level.curtain - 1 + t) / Settings.waitCurtain) * 0.5 * Settings.displaySize 88 | 89 | canvas.fillStyle = FAILURE_BACK 90 | canvas.fillRect(0, 0, Settings.screenWidth, 3) 91 | 92 | _paintCurtain(canvas, level.curtainPicture, 0, Settings.screenHeight, width) 93 | _paintCurtain(canvas, level.curtainPicture, Settings.screenWidth, 0, -width) 94 | } 95 | 96 | else if (level.state === LevelState.RESTARTING) { 97 | width = easeInQuad((level.curtain + 1 - t) / Settings.waitCurtain) * 0.5 * Settings.displaySize 98 | 99 | _paintCurtain(canvas, level.curtainPicture, 0.5 * Settings.screenWidth, 0.5 * Settings.screenHeight, -width, width) 100 | } 101 | } 102 | 103 | function _paintCurtain(canvas: CanvasRenderingContext2D, picture: HTMLCanvasElement, x: number, y: number, width: number, start = 0) { 104 | canvas.save() 105 | 106 | canvas.translate(x, y) 107 | canvas.rotate(-0.5124) // Math.atan2(-540, 960) 108 | 109 | canvas.beginPath() 110 | canvas.lineTo(start, -500) 111 | canvas.lineTo(width, -500) 112 | canvas.lineTo(width, 1000) 113 | canvas.lineTo(start, 1000) 114 | canvas.closePath() 115 | 116 | canvas.restore() 117 | 118 | canvas.save() 119 | 120 | canvas.clip() 121 | 122 | // canvas.fillStyle = FAILURE_BACK 123 | // canvas.fillRect(0, 0, Settings.screenWidth, Settings.screenHeight) 124 | canvas.drawImage(picture, 0, 0, Settings.screenWidth, Settings.screenHeight) 125 | 126 | canvas.restore() 127 | } 128 | -------------------------------------------------------------------------------- /typescript/BigBrother.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | /* Traced using the Polar Bears tool: 5 | * https://codepen.io/mvasilkov/details/VwaMMPK 6 | */ 7 | const CHROME_YELLOW = [ 8 | [0, 0, 1, 0], 9 | [1, 1, 1, 0], 10 | [2, 2, 1, 0], 11 | [3, 3, 1, 0], 12 | [4, 4, 1, 0], 13 | // [4, 5, 0.88, 0.12], 14 | [5, 5, 1, 0], // Intentional overlap. 15 | [1, 2, 0.31, 0.15], 16 | [12, 11, 0.44, 0.01], 17 | // [15, 14, 0.78, 0.22], 18 | [14, 14, 1, 0], // Intentional overlap. 19 | [15, 15, 1, 0], 20 | ] 21 | 22 | const CHROME_GREEN = [ 23 | [5, 5, 1, 0], 24 | [6, 6, 1, 0], 25 | [7, 7, 1, 0], 26 | [8, 8, 1, 0], 27 | [9, 9, 1, 0], 28 | // [9, 10, 0.56, 0.44], 29 | [10, 10, 1, 0], // Intentional overlap. 30 | [7, 6, 0.31, 0.15], 31 | [1, 2, 0.31, 0.15], 32 | [4, 5, 0.88, 0.12], 33 | ] 34 | 35 | const CHROME_RED = [ 36 | [10, 10, 1, 0], 37 | [11, 11, 1, 0], 38 | [12, 12, 1, 0], 39 | [13, 13, 1, 0], 40 | [14, 14, 1, 0], 41 | [15, 14, 0.78, 0.22], 42 | [12, 11, 0.44, 0.01], 43 | [7, 6, 0.31, 0.15], 44 | [9, 10, 0.56, 0.44], 45 | ] 46 | 47 | const CHROME_BLUE = [ 48 | [0, 1, 0.45, 0], 49 | [1, 0, 0.44, 0.01], 50 | [2, 3, 0.44, 0.01], 51 | [3, 4, 0.44, 0.01], 52 | [4, 3, 0.45, 0], 53 | [5, 6, 0.44, 0.01], 54 | [6, 7, 0.45, 0.01], 55 | [7, 8, 0.44, 0.01], 56 | [8, 9, 0.46, 0], 57 | [9, 10, 0.45, 0.01], 58 | [10, 11, 0.45, 0.01], 59 | [11, 10, 0.45, 0], 60 | [12, 11, 0.44, 0.01], 61 | [13, 12, 0.44, 0.01], 62 | [14, 13, 0.44, 0.01], 63 | [15, 14, 0.44, 0.01], 64 | ] 65 | 66 | class BigBrother extends UserAgent { 67 | paint(canvas: CanvasRenderingContext2D, t: number) { 68 | this.interpolate(t) 69 | 70 | this.tracePath(canvas, CHROME_YELLOW) 71 | 72 | canvas.fillStyle = '#ffcd40' 73 | canvas.fill() 74 | 75 | this.tracePath(canvas, CHROME_GREEN) 76 | 77 | canvas.fillStyle = '#0f9d58' 78 | canvas.fill() 79 | 80 | this.tracePath(canvas, CHROME_RED) 81 | 82 | canvas.fillStyle = '#db4437' 83 | canvas.fill() 84 | 85 | this.tracePath(canvas, CHROME_BLUE) 86 | 87 | canvas.fillStyle = '#4285f4' 88 | canvas.fill() 89 | 90 | canvas.lineWidth = 2.5 91 | canvas.strokeStyle = '#f1f1f1' 92 | canvas.stroke() 93 | 94 | canvas.lineWidth = 1 // restore 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /typescript/Box.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Box extends NBody { 5 | constructor(scene: NScene, x: number, y: number, size: number, stiffness = 0.5, mass = 1) { 6 | super(scene, mass) 7 | 8 | this.center.set(x, y) 9 | this.halfExtents.set(size * 0.5, size * 0.5) 10 | 11 | const theta = HALFPI 12 | const r = this.halfExtents.length() 13 | 14 | // Create vertices. 15 | for (let i = 0; i < 4; ++i) { 16 | const a = theta * i + FOURTHPI 17 | new NVertex(this, x + r * Math.cos(a), y + r * Math.sin(a)) 18 | } 19 | 20 | // Create constraints. 21 | for (let i = 0; i < this.vertices.length - 1; ++i) { 22 | for (let j = i + 1; j < this.vertices.length; ++j) { 23 | new NConstraint(this, this.vertices[i], this.vertices[j], j === i + 1, stiffness) 24 | } 25 | } 26 | } 27 | } 28 | 29 | Box.prototype.paint = Wall.prototype.paint 30 | -------------------------------------------------------------------------------- /typescript/Firefox.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | /* Traced using the Polar Bears tool: 5 | * https://codepen.io/mvasilkov/details/VwaMMPK 6 | */ 7 | const FIREFOX = [ 8 | [0, 0, 1, 0], 9 | [1, 1, 1, 0], 10 | [2, 2, 1, 0], 11 | [3, 3, 1, 0], 12 | [4, 4, 1, 0], 13 | [5, 5, 1, 0], 14 | [6, 6, 1, 0], 15 | [7, 7, 1, 0], 16 | [8, 8, 1, 0], 17 | [9, 9, 1, 0], 18 | [10, 9, 0.56, 0.44], 19 | [10, 11, 0.75, 0.21], 20 | [10, 9, 0.59, 0.18], 21 | [11, 10, 0.6, 0.16], 22 | [10, 11, 0.39, 0.16], 23 | [12, 11, 0.2, 0], 24 | [8, 7, 0.25, 0.22], 25 | [7, 8, 0.46, 0.01], 26 | [6, 7, 0.46, 0.01], 27 | [5, 6, 0.46, 0.01], 28 | [4, 3, 0.46, 0], 29 | [3, 4, 0.46, 0], 30 | [2, 1, 0.45, 0], 31 | [1, 0, 0.42, 0.01], 32 | [0, 15, 0.4, 0.01], 33 | [15, 14, 0.39, 0.01], 34 | [14, 13, 0.37, 0.01], 35 | [13, 14, 0.21, 0.17], 36 | [14, 13, 0.43, 0.01], 37 | [15, 14, 0.35, 0.17], 38 | [14, 13, 0.51, 0.01], 39 | [13, 12, 0.6, 0.01], 40 | [13, 12, 0.52, 0.34], 41 | [13, 12, 0.81, 0.31], 42 | [13, 12, 1, 0.01], 43 | [14, 15, 0.65, 0.25], 44 | [15, 0, 0.85, 0.04], 45 | [15, 14, 0.54, 0.4], 46 | [14, 15, 0.78, 0.19], 47 | [14, 15, 0.52, 0.48], 48 | [15, 15, 1, 0], 49 | ] 50 | 51 | const FIREFOX_BACK = canvas.createRadialGradient( 52 | 256, 512 + 64, 256, 53 | 256, -256 + 64, 256 54 | ) 55 | FIREFOX_BACK.addColorStop(1 - 0.7, '#e31587') 56 | FIREFOX_BACK.addColorStop(1 - 0.53, '#ff3647') 57 | FIREFOX_BACK.addColorStop(1 - 0.37, '#ff980e') 58 | FIREFOX_BACK.addColorStop(1 - 0.05, '#fff44f') 59 | 60 | const EARTH_BACK = canvas.createLinearGradient( 61 | 0, Settings.screenHeight, 62 | Settings.screenWidth, 0 63 | ) 64 | // Colors: https://webgradients.com/ '019 Malibu Beach' 65 | EARTH_BACK.addColorStop(0, '#4facfe') 66 | EARTH_BACK.addColorStop(1, '#00f2fe') 67 | 68 | class Firefox extends UserAgent { 69 | paint(canvas: CanvasRenderingContext2D, t: number) { 70 | this.interpolate(t) 71 | 72 | // Paint the Earth. 73 | canvas.beginPath() 74 | 75 | for (const vert of this.vertices) { 76 | canvas.lineTo(vert.interpolated.x, vert.interpolated.y) 77 | } 78 | 79 | canvas.closePath() 80 | 81 | canvas.fillStyle = EARTH_BACK 82 | canvas.fill() 83 | 84 | // Paint the panda. 85 | this.tracePath(canvas, FIREFOX) 86 | 87 | canvas.save() 88 | enclose(this.center.x - this.halfExtents.x, this.center.y - this.halfExtents.y, 89 | this.center.x + this.halfExtents.x, this.center.y + this.halfExtents.y) 90 | 91 | canvas.fillStyle = FIREFOX_BACK 92 | canvas.fill() 93 | 94 | canvas.restore() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /typescript/FiringPin.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class FiringPin extends NBody { 5 | constructor(scene: NScene, x: number, y: number, r: number, angle = 0, stiffness = 1, mass = 9) { 6 | super(scene, mass) 7 | 8 | const theta = HALFPI 9 | 10 | // Create vertices. 11 | for (let i = 0; i < 4; ++i) { 12 | const a = theta * i + angle 13 | new NVertex(this, x + r * Math.cos(a), y + r * Math.sin(a)) 14 | } 15 | 16 | // Create constraints. 17 | for (let i = 0; i < this.vertices.length - 1; ++i) { 18 | for (let j = i + 1; j < this.vertices.length; ++j) { 19 | new NConstraint(this, this.vertices[i], this.vertices[j], j === i + 1, stiffness) 20 | } 21 | } 22 | } 23 | 24 | retract() { 25 | // undo vertices 26 | this.scene.vertices.length -= this.vertices.length 27 | 28 | // undo constraints 29 | this.scene.constraints.length -= this.constraints.length 30 | 31 | // undo body 32 | this.scene.bodies.pop() 33 | } 34 | 35 | paint() { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /typescript/InternetExplorer.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | /* Traced using the Polar Bears tool: 5 | * https://codepen.io/mvasilkov/details/VwaMMPK 6 | */ 7 | const INTERNET_EXPLORER = [ 8 | [1, 1, 1, 0], 9 | [2, 2, 1, 0], 10 | [3, 3, 1, 0], 11 | [4, 4, 1, 0], 12 | [5, 5, 1, 0], 13 | [6, 6, 1, 0], 14 | [7, 7, 1, 0], 15 | [8, 8, 1, 0], 16 | [9, 9, 1, 0], 17 | [10, 10, 1, 0], 18 | [11, 11, 1, 0], 19 | [12, 12, 1, 0], 20 | [13, 13, 1, 0], 21 | [14, 14, 1, 0], 22 | [15, 15, 1, 0], 23 | [0, 0, 1, 0], 24 | [0, 1, 0.68, 0.33], 25 | [8, 7, 0.48, 0.33], 26 | [8, 9, 0.43, 0.38], 27 | [15, 0, 0.38, 0.08], 28 | [15, 14, 0.46, 0], 29 | [14, 13, 0.51, 0.01], 30 | [13, 12, 0.54, 0.01], 31 | [12, 11, 0.55, 0.01], 32 | [11, 12, 0.54, 0.01], 33 | [10, 11, 0.51, 0], 34 | [9, 8, 0.47, 0], 35 | [9, 8, 0.38, 0.09], 36 | [7, 8, 0.33, 0.14], 37 | [7, 8, 0.46, 0.01], 38 | [6, 7, 0.51, 0.01], 39 | [5, 6, 0.55, 0], 40 | [4, 5, 0.54, 0.01], 41 | [3, 4, 0.54, 0.01], 42 | [2, 1, 0.52, 0], 43 | [2, 1, 0.41, 0.11], 44 | [1, 0, 0.86, 0.14], 45 | ] 46 | 47 | const INTERNET_EXPLORER_BACK = canvas.createRadialGradient( 48 | 256, 256, 363, // Math.ceil(Math.sqrt(2 * 65536)), 49 | 256, 128, 0 50 | ) 51 | INTERNET_EXPLORER_BACK.addColorStop(1 - 1, '#0d79c8') 52 | // INTERNET_EXPLORER_BACK.addColorStop(1 - 0.9544, '#1c87cf') 53 | // INTERNET_EXPLORER_BACK.addColorStop(1 - 0.8397, '#3ea5dd') 54 | // INTERNET_EXPLORER_BACK.addColorStop(1 - 0.7163, '#59bee9') 55 | // INTERNET_EXPLORER_BACK.addColorStop(1 - 0.5832, '#6ed2f2') 56 | // INTERNET_EXPLORER_BACK.addColorStop(1 - 0.4357, '#7ddff9') 57 | INTERNET_EXPLORER_BACK.addColorStop(1 - 0.2624, '#86e8fd') 58 | INTERNET_EXPLORER_BACK.addColorStop(1 - 0, '#89eafe') 59 | 60 | class InternetExplorer extends UserAgent { 61 | paint(canvas: CanvasRenderingContext2D, t: number) { 62 | this.interpolate(t) 63 | 64 | this.tracePath(canvas, INTERNET_EXPLORER) 65 | 66 | canvas.save() 67 | enclose(this.center.x - this.halfExtents.x, this.center.y - this.halfExtents.y, 68 | this.center.x + this.halfExtents.x, this.center.y + this.halfExtents.y) 69 | 70 | canvas.fillStyle = INTERNET_EXPLORER_BACK // '#0078d7' 71 | canvas.fill() 72 | 73 | canvas.restore() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /typescript/Level.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const enum LevelState { 5 | INITIAL = 0, 6 | AIMING, 7 | FIRING, 8 | WAITING, 9 | FAILING, 10 | RESTARTING, 11 | WINNING, 12 | } 13 | 14 | class Level extends NScene { 15 | startingPoint: NVec2 16 | reticle: Reticle 17 | projectile: UserAgent 18 | firingPin: FiringPin | null 19 | website: Website 20 | state: LevelState 21 | duration: number 22 | waited: number 23 | curtain: number 24 | curtainPicture: HTMLCanvasElement 25 | autoWin: boolean 26 | 27 | static getUserAgent() { 28 | if (location.search.match(/firefox=1/) !== null) return Firefox 29 | if (location.search.match(/piracy=1/) !== null) return BigBrother 30 | if (document.monetization && document.monetization.state === 'started') return BigBrother 31 | return Firefox 32 | } 33 | 34 | constructor(startingPoint: NVec2, curtain = 0) { 35 | super() 36 | this.startingPoint = startingPoint 37 | this.reticle = new Reticle(this, startingPoint) 38 | this.projectile = new ((this.constructor).getUserAgent()) 39 | (this, startingPoint.x, startingPoint.y) // 32, 16, 0.016 40 | this.firingPin = null 41 | this.website = new Website 42 | this.state = LevelState.INITIAL 43 | this.duration = Settings.waitLevel 44 | this.waited = 0 45 | this.curtain = curtain 46 | this.curtainPicture = FAILURE_PICTURE 47 | this.autoWin = false 48 | } 49 | 50 | updateTargeting(pos: IVec2) { 51 | this.reticle.lastPosition.setTo(pos) 52 | } 53 | 54 | launch(): boolean { 55 | let start: IVec2 56 | let length: number 57 | 58 | register0.setSubtract(this.reticle.lastPosition, 59 | start = this.reticle.startingVertex.position) 60 | 61 | if ((length = register0.length()) < 16) return false 62 | 63 | register0.scalarMult(inverseRescale(length, 16, 64 | Settings.targetReleaseDist, 10, 30) / length) 65 | 66 | this.firingPin = new FiringPin(this, register0.x + start.x, register0.y + start.y, 67 | 32, Math.atan2(register0.y, register0.x) + FOURTHPI, 1, 9999) 68 | 69 | return true 70 | } 71 | 72 | getIndex() { 73 | return 0 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /typescript/Main.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const Levels = [ 5 | Level, 6 | TheWall, 7 | Opening, 8 | Breach, 9 | Banned, 10 | Reversal, 11 | Moving, 12 | Distancing, 13 | Hopeless, 14 | End, 15 | ] 16 | 17 | let activeLevel: Level 18 | let nextLevel: Level 19 | 20 | (function () { 21 | const startingPoint = new NVec2(350, Settings.screenHeight * 0.5) 22 | const captureDistSquared = Settings.targetCaptureDist ** 2 23 | const releaseDistSquared = Settings.targetReleaseDist ** 2 24 | 25 | let updatesToRetractFiringPin: number 26 | let updatesToWin: number 27 | let panningCounter: number 28 | 29 | activeLevel = new Levels[0](startingPoint) 30 | 31 | function update() { 32 | activeLevel.integrate() 33 | activeLevel.solve() 34 | 35 | if (activeLevel.state === LevelState.FIRING) { 36 | if (--updatesToRetractFiringPin <= 0) { 37 | activeLevel.firingPin!.retract() 38 | activeLevel.firingPin = null 39 | activeLevel.state = LevelState.WAITING 40 | updatesToWin = 2 41 | } 42 | } 43 | 44 | else if (activeLevel.state === LevelState.WAITING) { 45 | if (++activeLevel.waited >= activeLevel.duration) { 46 | if (activeLevel.autoWin) { 47 | activeLevel.state = LevelState.WINNING 48 | nextLevel = new Levels[(activeLevel.getIndex() + 1) % Levels.length](startingPoint) 49 | panningCounter = 0 50 | // Reset pointer. 51 | pointer.dragging = false 52 | pointer.vertex = undefined 53 | sound(sndWin) 54 | } 55 | else { 56 | activeLevel.state = LevelState.FAILING 57 | sound(sndFail) 58 | } 59 | } 60 | else if (activeLevel.website.contains(activeLevel.projectile.center)) { 61 | if (--updatesToWin <= 0) { 62 | activeLevel.state = LevelState.WINNING 63 | nextLevel = new Levels[(activeLevel.getIndex() + 1) % Levels.length](startingPoint) 64 | panningCounter = 0 65 | // Reset pointer. 66 | pointer.dragging = false 67 | pointer.vertex = undefined 68 | sound(sndWin) 69 | } 70 | } 71 | } 72 | 73 | else if (activeLevel.state === LevelState.FAILING) { 74 | if (++activeLevel.curtain >= Settings.waitCurtain) { 75 | activeLevel = new Levels[activeLevel.getIndex()](startingPoint, Settings.waitCurtain) 76 | activeLevel.state = LevelState.RESTARTING 77 | // Reset pointer. 78 | pointer.dragging = false 79 | pointer.vertex = undefined 80 | } 81 | } 82 | 83 | else if (activeLevel.state === LevelState.RESTARTING) { 84 | if (--activeLevel.curtain <= 0) { 85 | activeLevel.state = LevelState.INITIAL 86 | } 87 | } 88 | 89 | else if (activeLevel.state === LevelState.WINNING) { 90 | if (++panningCounter > Settings.waitNextLevel) { 91 | activeLevel = nextLevel 92 | } 93 | } 94 | } 95 | 96 | function render(t: number) { 97 | // Panning variables. 98 | let tPan: number 99 | let sPan: number 100 | 101 | // Panning part 1. 102 | if (activeLevel.state === LevelState.WINNING) { 103 | canvas.fillStyle = EARTH_BACK 104 | canvas.fillRect(0, 0, Settings.screenWidth, Settings.screenHeight) 105 | 106 | canvas.save() 107 | 108 | tPan = (panningCounter - 1 + t) / Settings.waitNextLevel 109 | sPan = lerp(1, 0.5, easeInOutQuad(tPan)) 110 | 111 | canvas.translate(Settings.screenWidth * 0.5, Settings.screenHeight * 0.5) 112 | canvas.scale(sPan, sPan) 113 | canvas.translate(-Settings.screenWidth * 0.5, -Settings.screenHeight * 0.5) 114 | 115 | canvas.translate(lerp(0, -Settings.screenWidth, easeInOutQuad(tPan)), 0) 116 | 117 | canvas.beginPath() 118 | 119 | canvas.rect(0, 0, Settings.screenWidth, Settings.screenHeight) 120 | 121 | canvas.clip() 122 | } 123 | 124 | // #region Pointer events. 125 | else if (pointer.dragging && activeLevel.state !== LevelState.RESTARTING) { 126 | if (!pointer.vertex && startingPoint.distanceSquared(pointer) <= captureDistSquared) { 127 | pointer.vertex = activeLevel.reticle.targetingVertex 128 | if (activeLevel.state === LevelState.INITIAL) 129 | activeLevel.state = LevelState.AIMING 130 | } 131 | 132 | if (pointer.vertex) { 133 | const pos = activeLevel.reticle.targetingVertex.position 134 | pos.setTo(pointer) 135 | 136 | const dist = startingPoint.distanceSquared(pos) 137 | if (dist > releaseDistSquared) { 138 | pos.subtract(startingPoint) 139 | pos.scalarMult(Settings.targetReleaseDist / Math.sqrt(dist)) 140 | pos.add(startingPoint) 141 | } 142 | 143 | activeLevel.updateTargeting(pos) 144 | } 145 | } 146 | else if (activeLevel.state === LevelState.AIMING) { 147 | if (activeLevel.launch()) { 148 | activeLevel.state = LevelState.FIRING 149 | updatesToRetractFiringPin = 4 150 | sound(sndLaunch) 151 | } 152 | else activeLevel.state = LevelState.INITIAL 153 | } 154 | // #endregion 155 | 156 | // #region Paint active level. 157 | paintBackground(canvas, t, activeLevel) 158 | 159 | activeLevel.website.paint(canvas, t) 160 | 161 | for (const b of activeLevel.bodies) { 162 | b.paint(canvas, t) 163 | } 164 | // #endregion 165 | 166 | paintCurtain(canvas, t, activeLevel) 167 | 168 | // Panning part 2. 169 | if (activeLevel.state === LevelState.WINNING) { 170 | canvas.restore() 171 | 172 | canvas.save() 173 | 174 | canvas.translate(lerp(Settings.screenWidth, 0, easeInOutQuad(tPan!)), 0) 175 | 176 | // #region Paint next level. 177 | paintBackground(canvas, t, nextLevel) 178 | 179 | nextLevel.website.paint(canvas, t) 180 | 181 | for (const b of nextLevel.bodies) { 182 | b.paint(canvas, t) 183 | } 184 | // #endregion 185 | 186 | canvas.restore() 187 | } 188 | } 189 | 190 | handleResize() 191 | 192 | startMainloop(update, render) 193 | })() 194 | -------------------------------------------------------------------------------- /typescript/MovingWebsite.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const enum MW { 5 | // start 6 | x0 = Settings.screenWidth - Settings.websiteWidth, 7 | y0 = (Settings.screenHeight - Settings.websiteHeight) * 0.5, 8 | width0 = Settings.websiteWidth, 9 | height0 = Settings.websiteHeight, 10 | // corner 11 | size1 = (Settings.websiteWidth + Settings.websiteHeight) * 0.5, 12 | x1 = Settings.screenWidth - size1, 13 | y1 = Settings.screenHeight - size1, 14 | width1 = size1, 15 | height1 = size1, 16 | // end 17 | x2 = 350 - Settings.websiteHeight * 0.5, 18 | y2 = Settings.screenHeight - Settings.websiteWidth, 19 | width2 = Settings.websiteHeight, 20 | height2 = Settings.websiteWidth, 21 | } 22 | 23 | class MovingWebsite extends Website { 24 | n: number 25 | old: { 26 | x: number 27 | y: number 28 | width: number 29 | height: number 30 | } 31 | 32 | constructor() { 33 | super() 34 | 35 | this.n = 0 36 | this.old = { 37 | x: this.x, 38 | y: this.y, 39 | width: this.width, 40 | height: this.height, 41 | } 42 | } 43 | 44 | update() { 45 | if (this.n > 80) return 46 | ++this.n 47 | 48 | this.old.x = this.x 49 | this.old.y = this.y 50 | this.old.width = this.width 51 | this.old.height = this.height 52 | 53 | if (this.n <= 25) { 54 | const t = this.n * 0.04 55 | 56 | this.x = lerp(MW.x0, MW.x1, easeInQuad(t)) 57 | this.y = lerp(MW.y0, MW.y1, easeInQuad(t)) 58 | this.width = lerp(MW.width0, MW.width1, easeInQuad(t)) 59 | this.height = lerp(MW.height0, MW.height1, easeInQuad(t)) 60 | } 61 | else { // this.n <= 80 62 | const t = (this.n - 25) / 55 63 | 64 | this.x = lerp(MW.x1, MW.x2, easeOutQuad(t)) 65 | this.y = lerp(MW.y1, MW.y2, easeOutQuad(t)) 66 | this.width = lerp(MW.width1, MW.width2, easeOutQuad(t)) 67 | this.height = lerp(MW.height1, MW.height2, easeOutQuad(t)) 68 | } 69 | } 70 | 71 | paint(canvas: CanvasRenderingContext2D, t: number) { 72 | let x: number 73 | let y: number 74 | let width: number 75 | let height: number 76 | 77 | if (this.n > 80) { 78 | x = this.x 79 | y = this.y 80 | width = this.width 81 | height = this.height 82 | } 83 | else { 84 | x = lerp(this.old.x, this.x, t) 85 | y = lerp(this.old.y, this.y, t) 86 | width = lerp(this.old.width, this.width, t) 87 | height = lerp(this.old.height, this.height, t) 88 | } 89 | 90 | canvas.fillStyle = '#f1f1f1' 91 | canvas.fillRect(x, y, width, height) 92 | 93 | canvas.drawImage(WEBSITE_PICTURE, 94 | x + (width - Settings.websitePicWidth) * 0.5, 95 | y + (height - Settings.websitePicHeight) * 0.5, 96 | Settings.websitePicWidth, Settings.websitePicHeight) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /typescript/NoWebsite.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class NoWebsite extends Website { 5 | contains() { 6 | return false 7 | } 8 | 9 | paint() { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /typescript/Reticle.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Reticle extends NBody { 5 | startingVertex: NVertex 6 | targetingVertex: NVertex 7 | lastPosition: NVec2 8 | 9 | constructor(scene: NScene, startingPoint: IVec2) { 10 | super(scene) 11 | this.startingVertex = new NStaticVertex(this, startingPoint.x, startingPoint.y) 12 | this.targetingVertex = new NVertex(this, startingPoint.x - 0.001, startingPoint.y) 13 | this.lastPosition = new NVec2(startingPoint.x, startingPoint.y) 14 | 15 | const cons = new NConstraint(this, this.startingVertex, this.targetingVertex, 16 | false, Settings.reticleStiffness) 17 | 18 | // Make the starting vertex stay in place. 19 | const originalSolve = cons.solve 20 | cons.solve = () => { 21 | if (pointer.vertex) return // Do nothing while dragging. 22 | originalSolve.call(cons) 23 | this.startingVertex.position.setTo(startingPoint) 24 | } 25 | } 26 | 27 | paint(canvas: CanvasRenderingContext2D, t: number) { 28 | // Interpolate vertices. 29 | this.targetingVertex.interpolate(t) 30 | 31 | const start = this.startingVertex.position 32 | const pos = this.targetingVertex.interpolated 33 | 34 | canvas.beginPath() 35 | canvas.moveTo(pos.x, pos.y) 36 | canvas.lineTo(start.x, start.y) 37 | canvas.strokeStyle = FAILURE_BACK 38 | canvas.stroke() 39 | 40 | canvas.beginPath() 41 | canvas.arc(pos.x, pos.y, 9, 0, TWOPI) 42 | canvas.arc(start.x, start.y, 4, 0, TWOPI) 43 | canvas.fillStyle = FAILURE_BACK 44 | canvas.fill() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /typescript/UserAgent.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class UserAgent extends NBall { 5 | /** Interpolated vertices relative to center. */ 6 | relInterp: NVec2[] 7 | 8 | constructor(scene: NScene, x: number, y: number) { 9 | super(scene, x, y, 32, 16, 0.016) 10 | 11 | this.relInterp = [] 12 | for (let n = 0; n < 16; ++n) { 13 | this.relInterp.push(new NVec2) 14 | } 15 | } 16 | 17 | /** Interpolate vertices. */ 18 | interpolate(t: number) { 19 | // Interpolate `center` and `halfExtents`. 20 | let vert = this.vertices[0] 21 | vert.interpolate(t) 22 | 23 | let p = vert.interpolated 24 | let xMin = p.x 25 | let xMax = p.x 26 | let yMin = p.y 27 | let yMax = p.y 28 | 29 | for (let n = 1; n < 16; ++n) { 30 | vert = this.vertices[n] 31 | vert.interpolate(t) 32 | p = vert.interpolated 33 | 34 | if (p.x < xMin) xMin = p.x 35 | else if (p.x > xMax) xMax = p.x 36 | 37 | if (p.y < yMin) yMin = p.y 38 | else if (p.y > yMax) yMax = p.y 39 | } 40 | 41 | this.center.set((xMin + xMax) * 0.5, (yMin + yMax) * 0.5) 42 | this.halfExtents.set((xMax - xMin) * 0.5, (yMax - yMin) * 0.5) 43 | 44 | // Center vertices. 45 | for (let n = 0; n < 16; ++n) { 46 | this.relInterp[n].setSubtract(this.vertices[n].interpolated, this.center) 47 | } 48 | } 49 | 50 | tracePath(canvas: CanvasRenderingContext2D, path: number[][]) { 51 | canvas.beginPath() 52 | 53 | for (const [v1, v2, a, b] of path) { 54 | register0.setScalarMult(this.relInterp[v1], a) 55 | register1.setScalarMult(this.relInterp[v2], b) 56 | register0.add(register1) 57 | register0.add(this.center) 58 | canvas.lineTo(register0.x, register0.y) 59 | } 60 | 61 | canvas.closePath() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /typescript/Wall.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Wall extends NBody { 5 | constructor(scene: NScene, x: number, y: number, stiffness = 1, mass = 9) { 6 | super(scene, mass) 7 | 8 | // Create vertices. 9 | const v0 = new NStaticVertex(this, x, y) 10 | const v1 = new NStaticVertex(this, x + 64, y) 11 | const v2 = new NStaticVertex(this, x + 64, y + 256) 12 | const v3 = new NStaticVertex(this, x, y + 256) 13 | 14 | // Create edges. 15 | new NConstraint(this, v0, v1, true, stiffness) 16 | new NConstraint(this, v1, v2, true, stiffness) 17 | new NConstraint(this, v2, v3, true, stiffness) 18 | new NConstraint(this, v3, v0, true, stiffness) 19 | 20 | // Create constraints. 21 | new NConstraint(this, v0, v2, false, stiffness) 22 | new NConstraint(this, v1, v3, false, stiffness) 23 | 24 | this.center.set(x + 32, y + 128) 25 | this.halfExtents.set(32, 128) 26 | } 27 | 28 | rotate(angle: number) { 29 | const cos = Math.cos(angle) 30 | const sin = Math.sin(angle) 31 | 32 | for (const vert of this.vertices) { 33 | register0.setSubtract(vert.position, this.center) 34 | vert.set( 35 | this.center.x + register0.x * cos - register0.y * sin, 36 | this.center.y + register0.x * sin + register0.y * cos 37 | ) 38 | vert.integrate() 39 | } 40 | } 41 | 42 | paint(canvas: CanvasRenderingContext2D, t: number) { 43 | // Interpolate vertices. 44 | for (const vert of this.vertices) vert.interpolate(t) 45 | 46 | // Trace path. 47 | canvas.beginPath() 48 | 49 | for (let n = 0; n < 4; ++n) { 50 | canvas.lineTo(this.vertices[n].interpolated.x, this.vertices[n].interpolated.y) 51 | } 52 | 53 | canvas.closePath() 54 | 55 | // Paint background. 56 | canvas.save() 57 | 58 | canvas.clip() 59 | 60 | canvas.drawImage(WALL_PICTURE, 0, 0, Settings.screenWidth, Settings.screenHeight) 61 | 62 | canvas.restore() 63 | 64 | // Paint edges. 65 | canvas.strokeStyle = FAILURE_BACK 66 | canvas.stroke() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /typescript/Website.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const G_BLUE = '#4285f4' 5 | const G_RED = '#ea4335' 6 | const G_YELLOW = '#fbbc05' 7 | const G_GREEN = '#34a853' 8 | 9 | const WEBSITE_PICTURE = prerender(Settings.websitePicWidth, Settings.websitePicHeight, canvas => { 10 | canvas.fillStyle = '#f1f1f1' 11 | canvas.fillRect(0, 0, Settings.websitePicWidth, Settings.websitePicHeight) 12 | 13 | canvas.save() 14 | 15 | canvas.translate(1, 0) 16 | canvas.scale(5, 5) 17 | // 'G' 18 | canvas.fillStyle = G_BLUE 19 | canvas.fillRect(0, 0, 3, 3) 20 | // 'o' 21 | canvas.fillStyle = G_RED 22 | canvas.fillRect(4, 1, 2, 2) 23 | // 'o' 24 | canvas.fillStyle = G_YELLOW 25 | canvas.fillRect(7, 1, 2, 2) 26 | // 'g' 27 | canvas.fillStyle = G_BLUE 28 | canvas.fillRect(10, 1, 2, 3) 29 | // 'l' 30 | canvas.fillStyle = G_GREEN 31 | canvas.fillRect(13, 0, 1, 3) 32 | // 'e' 33 | canvas.fillStyle = G_RED 34 | canvas.fillRect(15, 1, 2, 2) 35 | 36 | canvas.restore() 37 | 38 | // Search bar and buttons. 39 | canvas.save() 40 | 41 | canvas.translate(0, 30) 42 | canvas.scale(3, 3) 43 | 44 | canvas.fillStyle = '#ddd' 45 | canvas.fillRect(0, 0, 29, 4) 46 | 47 | canvas.fillStyle = '#cdcdcd' 48 | canvas.fillRect(3, 7, 10, 4) 49 | canvas.fillRect(16, 7, 10, 4) 50 | 51 | canvas.restore() 52 | }) 53 | 54 | class Website { 55 | x: number 56 | y: number 57 | width: number 58 | height: number 59 | 60 | constructor() { 61 | this.x = Settings.screenWidth - Settings.websiteWidth 62 | this.y = (Settings.screenHeight - Settings.websiteHeight) * 0.5 63 | this.width = Settings.websiteWidth 64 | this.height = Settings.websiteHeight 65 | } 66 | 67 | update() { 68 | } 69 | 70 | contains(p: IVec2): boolean { 71 | return ( 72 | p.x >= this.x && p.x - this.x < this.width && 73 | p.y >= this.y && p.y - this.y < this.height 74 | ) 75 | } 76 | 77 | paint(canvas: CanvasRenderingContext2D, _t: number) { 78 | canvas.fillStyle = '#f1f1f1' 79 | canvas.fillRect(this.x, this.y, this.width, this.height) 80 | 81 | canvas.drawImage(WEBSITE_PICTURE, 82 | this.x + (Settings.websiteWidth - Settings.websitePicWidth) * 0.5, 83 | this.y + (Settings.websiteHeight - Settings.websitePicHeight) * 0.5, 84 | Settings.websitePicWidth, Settings.websitePicHeight) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /typescript/WebsiteBox.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class WebsiteBox extends NBody { 5 | constructor(scene: NScene, x: number, y: number, stiffness = 0.5, mass = 1) { 6 | super(scene, mass) 7 | 8 | // Create vertices. 9 | const v0 = new NVertex(this, x, y) 10 | const v1 = new NVertex(this, x + Settings.websiteWidth, y) 11 | const v2 = new NVertex(this, x + Settings.websiteWidth, y + Settings.websiteHeight) 12 | const v3 = new NVertex(this, x, y + Settings.websiteHeight) 13 | 14 | // Create edges. 15 | new NConstraint(this, v0, v1, true, stiffness) 16 | new NConstraint(this, v1, v2, true, stiffness) 17 | new NConstraint(this, v2, v3, true, stiffness) 18 | new NConstraint(this, v3, v0, true, stiffness) 19 | 20 | // Create constraints. 21 | new NConstraint(this, v0, v2, false, stiffness) 22 | new NConstraint(this, v1, v3, false, stiffness) 23 | 24 | this.center.set(x + 0.5 * Settings.websiteWidth, y + 0.5 * Settings.websiteHeight) 25 | this.halfExtents.set(0.5 * Settings.websiteWidth, 0.5 * Settings.websiteHeight) 26 | } 27 | 28 | paint(canvas: CanvasRenderingContext2D, t: number) { 29 | // Interpolate vertices. 30 | for (const vert of this.vertices) vert.interpolate(t) 31 | 32 | // Trace path. 33 | canvas.beginPath() 34 | 35 | let xx = 0 36 | let yy = 0 37 | 38 | for (let n = 0; n < 4; ++n) { 39 | const p = this.vertices[n].interpolated 40 | 41 | canvas.lineTo(p.x, p.y) 42 | 43 | xx += p.x 44 | yy += p.y 45 | } 46 | 47 | canvas.closePath() 48 | 49 | // Paint background. 50 | canvas.fillStyle = '#f1f1f1' 51 | canvas.fill() 52 | 53 | // Paint website logo. 54 | register0.setSubtract(this.vertices[1].interpolated, this.vertices[0].interpolated) 55 | register1.setSubtract(this.vertices[2].interpolated, this.vertices[3].interpolated) 56 | 57 | canvas.save() 58 | 59 | canvas.translate(0.25 * xx, 0.25 * yy) 60 | canvas.rotate(0.5 * (Math.atan2(register0.y, register0.x) + Math.atan2(register1.y, register1.x))) 61 | 62 | canvas.drawImage(WEBSITE_PICTURE, 63 | -0.5 * Settings.websitePicWidth, 64 | -0.5 * Settings.websitePicHeight, 65 | Settings.websitePicWidth, Settings.websitePicHeight) 66 | 67 | canvas.restore() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /typescript/audio/audio.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const TEMPO_MUL = 120 / 118 5 | 6 | function noteFreq(n: number) { 7 | return 440 * Math.pow(2, (n - 69) / 12) 8 | } 9 | 10 | let ac: AudioContext 11 | let out: GainNode 12 | let songStart: number 13 | 14 | function audioInit() { 15 | ac = new AudioContext 16 | 17 | out = ac.createGain() 18 | out.gain.value = 0.4 19 | // out.connect(ac.destination) 20 | // return Promise.resolve() 21 | return reverb() 22 | } 23 | 24 | function playNote(n: number, start: number, end: number) { 25 | const freq = noteFreq(n) 26 | start *= TEMPO_MUL 27 | end *= TEMPO_MUL 28 | 29 | const osc = ac.createOscillator() 30 | osc.type = 'square' 31 | osc.frequency.value = freq 32 | decay(osc, start).connect(out) 33 | osc.start(songStart + start) 34 | osc.stop(songStart + end) 35 | } 36 | 37 | function decay(osc: OscillatorNode, start: number) { 38 | const env = ac.createGain() 39 | env.gain.setValueAtTime(0.5, songStart + start) 40 | env.gain.exponentialRampToValueAtTime(0.00001, songStart + start + 1.5 * TEMPO_MUL) 41 | osc.connect(env) 42 | return env 43 | } 44 | 45 | /* --- Experimental --- */ 46 | 47 | function reverb() { 48 | const conv = ac.createConvolver() 49 | const dry = ac.createGain() 50 | const wet = ac.createGain() 51 | 52 | dry.gain.value = 2 / 3 53 | wet.gain.value = 1 / 3 54 | out.connect(conv) 55 | out.connect(dry) 56 | conv.connect(wet) 57 | dry.connect(ac.destination) 58 | wet.connect(ac.destination) 59 | 60 | return new Promise(function (resolve) { 61 | generateReverb({ 62 | audioContext: ac, 63 | fadeIn: 0.00001, 64 | decay: 1.5 * TEMPO_MUL, 65 | lpFreqStart: 16000, 66 | lpFreqEnd: 1000, 67 | }, function (buf) { 68 | conv.buffer = buf 69 | resolve() 70 | }) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /typescript/audio/oborona.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | function playPart(n: number) { 5 | const hbar = n > 51 ? (n - 8) % 44 + 8 : n 6 | // console.log(`playPart n=${n} hbar=${hbar}`) 7 | 8 | switch (hbar) { 9 | case 0: // half-bar 1 10 | playNote(57, n + 0.0, n + 0.25) 11 | playNote(45, n + 0.0, n + 2.0) 12 | playNote(60, n + 0.25, n + 0.5) 13 | playNote(57, n + 0.5, n + 0.75) 14 | playNote(64, n + 0.75, n + 1.25) 15 | break 16 | case 1: // half-bar 2 17 | playNote(60, n + 0.25, n + 0.75) 18 | playNote(60, n + 0.75, n + 1.0) 19 | break 20 | case 2: // half-bar 3 21 | playNote(52, n + 0.0, n + 1.0) 22 | playNote(60, n + 0.25, n + 0.5) 23 | playNote(57, n + 0.5, n + 0.75) 24 | playNote(64, n + 0.75, n + 1.25) 25 | break 26 | case 3: // half-bar 4 27 | playNote(52, n + 0.0, n + 0.75) 28 | playNote(60, n + 0.25, n + 0.75) 29 | playNote(60, n + 0.75, n + 1.0) 30 | playNote(52, n + 0.75, n + 1.0) 31 | break 32 | case 4: // half-bar 5 33 | playNote(45, n + 0.0, n + 1.75) 34 | playNote(57, n + 0.25, n + 0.5) 35 | playNote(60, n + 0.5, n + 0.75) 36 | playNote(64, n + 0.75, n + 1.25) 37 | playNote(57, n + 0.75, n + 1.25) 38 | break 39 | case 5: // half-bar 6 40 | playNote(57, n + 0.25, n + 0.5) 41 | playNote(60, n + 0.5, n + 1.0) 42 | playNote(52, n + 0.75, n + 1.0) 43 | break 44 | case 6: // half-bar 7 45 | playNote(60, n + 0.0, n + 1.0) 46 | playNote(52, n + 0.0, n + 0.5) 47 | playNote(52, n + 0.5, n + 1.5) 48 | break 49 | case 7: // half-bar 8 50 | playNote(64, n + 0.0, n + 1.0) 51 | playNote(52, n + 0.5, n + 0.75) 52 | playNote(52, n + 0.75, n + 1.0) 53 | break 54 | case 8: // half-bar 9 55 | playNote(57, n + 0.0, n + 0.25) 56 | playNote(45, n + 0.0, n + 0.75) 57 | playNote(57, n + 0.25, n + 0.5) 58 | playNote(57, n + 0.5, n + 0.75) 59 | playNote(60, n + 0.75, n + 1.25) 60 | playNote(52, n + 0.75, n + 1.75) 61 | break 62 | case 9: // half-bar 10 63 | playNote(69, n + 0.25, n + 0.5) 64 | playNote(67, n + 0.5, n + 0.75) 65 | playNote(69, n + 0.75, n + 1.0) 66 | playNote(52, n + 0.75, n + 1.0) 67 | break 68 | case 10: // half-bar 11 69 | playNote(69, n + 0.0, n + 0.75) 70 | playNote(41, n + 0.0, n + 0.25) 71 | playNote(48, n + 0.25, n + 0.5) 72 | playNote(55, n + 0.5, n + 1.5) 73 | playNote(57, n + 0.75, n + 1.75) 74 | break 75 | case 11: // half-bar 12 76 | playNote(55, n + 0.5, n + 0.75) 77 | playNote(67, n + 0.75, n + 1.0) 78 | playNote(50, n + 0.75, n + 1.0) 79 | break 80 | case 12: // half-bar 13 81 | playNote(67, n + 0.0, n + 0.25) 82 | playNote(55, n + 0.0, n + 0.75) 83 | playNote(67, n + 0.25, n + 0.5) 84 | playNote(67, n + 0.5, n + 0.75) 85 | playNote(66, n + 0.75, n + 1.25) 86 | playNote(57, n + 0.75, n + 1.75) 87 | playNote(54, n + 0.75, n + 1.75) 88 | break 89 | case 13: // half-bar 14 90 | playNote(67, n + 0.25, n + 0.5) 91 | playNote(66, n + 0.5, n + 0.75) 92 | playNote(64, n + 0.75, n + 1.0) 93 | playNote(47, n + 0.75, n + 1.0) 94 | break 95 | case 14: // half-bar 15 96 | playNote(52, n + 0.0, n + 0.25) 97 | playNote(47, n + 0.0, n + 2.0) 98 | playNote(54, n + 0.25, n + 0.5) 99 | playNote(56, n + 0.5, n + 0.75) 100 | playNote(59, n + 0.75, n + 1.75) 101 | break 102 | case 15: // half-bar 16 103 | playNote(57, n + 0.75, n + 1.0) 104 | break 105 | case 16: // half-bar 17 106 | playNote(57, n + 0.0, n + 0.25) 107 | playNote(45, n + 0.0, n + 0.75) 108 | playNote(57, n + 0.25, n + 0.5) 109 | playNote(57, n + 0.5, n + 0.75) 110 | playNote(60, n + 0.75, n + 1.25) 111 | playNote(52, n + 0.75, n + 1.25) 112 | break 113 | case 17: // half-bar 18 114 | playNote(69, n + 0.25, n + 0.5) 115 | playNote(57, n + 0.25, n + 1.0) 116 | playNote(67, n + 0.5, n + 0.75) 117 | playNote(69, n + 0.75, n + 1.0) 118 | break 119 | case 18: // half-bar 19 120 | playNote(69, n + 0.0, n + 0.75) 121 | playNote(57, n + 0.0, n + 0.25) 122 | playNote(53, n + 0.0, n + 0.25) 123 | playNote(48, n + 0.0, n + 0.25) 124 | playNote(48, n + 0.25, n + 0.5) 125 | playNote(55, n + 0.5, n + 0.75) 126 | playNote(65, n + 0.75, n + 1.75) 127 | playNote(57, n + 0.75, n + 1.25) 128 | break 129 | case 19: // half-bar 20 130 | playNote(57, n + 0.25, n + 0.5) 131 | playNote(60, n + 0.5, n + 1.0) 132 | playNote(67, n + 0.75, n + 1.0) 133 | break 134 | case 20: // half-bar 21 135 | playNote(67, n + 0.0, n + 0.25) 136 | playNote(43, n + 0.0, n + 0.5) 137 | playNote(59, n + 0.25, n + 0.5) 138 | playNote(67, n + 0.5, n + 0.75) 139 | playNote(50, n + 0.5, n + 0.75) 140 | playNote(66, n + 0.75, n + 1.25) 141 | playNote(47, n + 0.75, n + 1.75) 142 | break 143 | case 21: // half-bar 22 144 | playNote(67, n + 0.25, n + 0.5) 145 | playNote(66, n + 0.5, n + 0.75) 146 | playNote(64, n + 0.75, n + 1.0) 147 | playNote(47, n + 0.75, n + 1.0) 148 | break 149 | case 22: // half-bar 23 150 | playNote(52, n + 0.0, n + 0.25) 151 | playNote(47, n + 0.0, n + 2.0) 152 | playNote(54, n + 0.25, n + 0.5) 153 | playNote(56, n + 0.5, n + 0.75) 154 | playNote(59, n + 0.75, n + 1.75) 155 | break 156 | case 23: // half-bar 24 157 | playNote(67, n + 0.75, n + 1.0) 158 | break 159 | case 24: // half-bar 25 160 | playNote(67, n + 0.0, n + 0.25) 161 | playNote(59, n + 0.0, n + 0.5) 162 | playNote(55, n + 0.0, n + 0.5) 163 | playNote(62, n + 0.25, n + 0.5) 164 | playNote(67, n + 0.5, n + 0.75) 165 | playNote(59, n + 0.5, n + 0.75) 166 | playNote(67, n + 0.75, n + 1.25) 167 | playNote(57, n + 0.75, n + 1.25) 168 | break 169 | case 25: // half-bar 26 170 | playNote(67, n + 0.25, n + 0.5) 171 | playNote(50, n + 0.25, n + 0.5) 172 | playNote(67, n + 0.5, n + 0.75) 173 | playNote(43, n + 0.5, n + 1.0) 174 | playNote(68, n + 0.75, n + 1.0) 175 | playNote(64, n + 0.75, n + 1.0) 176 | break 177 | case 26: // half-bar 27 178 | playNote(68, n + 0.0, n + 0.25) 179 | playNote(64, n + 0.0, n + 0.25) 180 | playNote(40, n + 0.0, n + 0.5) 181 | playNote(64, n + 0.25, n + 0.5) 182 | playNote(62, n + 0.5, n + 0.75) 183 | playNote(47, n + 0.5, n + 0.75) 184 | playNote(62, n + 0.75, n + 1.25) 185 | playNote(52, n + 0.75, n + 1.5) 186 | break 187 | case 27: // half-bar 28 188 | playNote(60, n + 0.25, n + 0.75) 189 | playNote(52, n + 0.5, n + 0.75) 190 | playNote(57, n + 0.75, n + 1.0) 191 | playNote(47, n + 0.75, n + 1.0) 192 | break 193 | case 28: // half-bar 29 194 | playNote(60, n + 0.0, n + 0.5) 195 | playNote(45, n + 0.0, n + 0.5) 196 | playNote(72, n + 0.5, n + 1.0) 197 | playNote(64, n + 0.5, n + 1.0) 198 | playNote(57, n + 0.5, n + 0.75) 199 | playNote(48, n + 0.75, n + 1.0) 200 | break 201 | case 29: // half-bar 30 202 | playNote(60, n + 0.0, n + 0.25) 203 | playNote(57, n + 0.0, n + 0.75) 204 | playNote(53, n + 0.0, n + 0.75) 205 | playNote(65, n + 0.25, n + 0.5) 206 | playNote(67, n + 0.5, n + 0.75) 207 | playNote(72, n + 0.75, n + 1.0) 208 | playNote(43, n + 0.75, n + 1.0) 209 | break 210 | case 30: // half-bar 31 211 | playNote(72, n + 0.0, n + 0.25) 212 | playNote(64, n + 0.0, n + 0.25) 213 | playNote(60, n + 0.0, n + 0.25) 214 | playNote(48, n + 0.0, n + 0.5) 215 | playNote(36, n + 0.0, n + 0.5) 216 | playNote(72, n + 0.25, n + 0.5) 217 | playNote(64, n + 0.25, n + 0.5) 218 | playNote(60, n + 0.25, n + 0.5) 219 | playNote(72, n + 0.5, n + 0.75) 220 | playNote(64, n + 0.5, n + 0.75) 221 | playNote(60, n + 0.5, n + 0.75) 222 | playNote(55, n + 0.5, n + 0.75) 223 | playNote(72, n + 0.75, n + 1.25) 224 | playNote(64, n + 0.75, n + 1.25) 225 | playNote(60, n + 0.75, n + 1.25) 226 | playNote(50, n + 0.75, n + 1.5) 227 | break 228 | case 31: // half-bar 32 229 | playNote(71, n + 0.25, n + 1.0) 230 | playNote(62, n + 0.25, n + 1.0) 231 | playNote(59, n + 0.25, n + 1.0) 232 | playNote(57, n + 0.5, n + 0.75) 233 | playNote(50, n + 0.75, n + 1.0) 234 | break 235 | case 32: // half-bar 33 236 | playNote(72, n + 0.0, n + 0.25) 237 | playNote(64, n + 0.0, n + 0.25) 238 | playNote(60, n + 0.0, n + 0.25) 239 | playNote(57, n + 0.0, n + 0.5) 240 | playNote(52, n + 0.0, n + 0.5) 241 | playNote(45, n + 0.0, n + 0.5) 242 | playNote(72, n + 0.25, n + 0.5) 243 | playNote(72, n + 0.5, n + 0.75) 244 | playNote(59, n + 0.5, n + 1.0) 245 | playNote(72, n + 0.75, n + 1.25) 246 | break 247 | case 33: // half-bar 34 248 | playNote(57, n + 0.0, n + 0.5) 249 | playNote(53, n + 0.0, n + 0.5) 250 | playNote(48, n + 0.0, n + 0.5) 251 | playNote(71, n + 0.25, n + 0.5) 252 | playNote(69, n + 0.5, n + 0.75) 253 | playNote(48, n + 0.5, n + 1.0) 254 | playNote(67, n + 0.75, n + 1.0) 255 | playNote(64, n + 0.75, n + 1.0) 256 | playNote(60, n + 0.75, n + 1.0) 257 | break 258 | case 34: // half-bar 35 259 | playNote(67, n + 0.0, n + 0.25) 260 | playNote(64, n + 0.0, n + 0.25) 261 | playNote(60, n + 0.0, n + 0.25) 262 | playNote(36, n + 0.0, n + 0.5) 263 | playNote(69, n + 0.25, n + 0.5) 264 | playNote(71, n + 0.5, n + 0.75) 265 | playNote(43, n + 0.5, n + 0.75) 266 | playNote(72, n + 0.75, n + 1.25) 267 | playNote(68, n + 0.75, n + 1.25) 268 | playNote(64, n + 0.75, n + 1.25) 269 | playNote(48, n + 0.75, n + 1.0) 270 | break 271 | case 35: // half-bar 36 272 | playNote(40, n + 0.0, n + 0.5) 273 | playNote(71, n + 0.25, n + 1.0) 274 | playNote(56, n + 0.5, n + 0.75) 275 | playNote(52, n + 0.75, n + 1.0) 276 | break 277 | case 36: // half-bar 37 278 | playNote(60, n + 0.0, n + 0.5) 279 | playNote(45, n + 0.0, n + 0.25) 280 | playNote(52, n + 0.25, n + 0.5) 281 | playNote(72, n + 0.5, n + 1.0) 282 | playNote(64, n + 0.5, n + 1.0) 283 | playNote(60, n + 0.5, n + 1.0) 284 | playNote(57, n + 0.5, n + 1.0) 285 | break 286 | case 37: // half-bar 38 287 | playNote(60, n + 0.0, n + 0.25) 288 | playNote(57, n + 0.0, n + 0.5) 289 | playNote(53, n + 0.0, n + 0.5) 290 | playNote(65, n + 0.25, n + 0.5) 291 | playNote(67, n + 0.5, n + 0.75) 292 | playNote(48, n + 0.5, n + 0.75) 293 | playNote(72, n + 0.75, n + 1.0) 294 | playNote(60, n + 0.75, n + 1.0) 295 | playNote(43, n + 0.75, n + 1.0) 296 | break 297 | case 38: // half-bar 39 298 | playNote(72, n + 0.0, n + 0.25) 299 | playNote(67, n + 0.0, n + 0.25) 300 | playNote(60, n + 0.0, n + 0.25) 301 | playNote(48, n + 0.0, n + 0.5) 302 | playNote(72, n + 0.25, n + 0.5) 303 | playNote(60, n + 0.25, n + 0.5) 304 | playNote(72, n + 0.5, n + 0.75) 305 | playNote(60, n + 0.5, n + 0.75) 306 | playNote(55, n + 0.5, n + 0.75) 307 | playNote(72, n + 0.75, n + 1.25) 308 | playNote(67, n + 0.75, n + 1.25) 309 | playNote(62, n + 0.75, n + 1.25) 310 | playNote(50, n + 0.75, n + 1.0) 311 | break 312 | case 39: // half-bar 40 313 | playNote(43, n + 0.0, n + 0.25) 314 | playNote(71, n + 0.25, n + 0.75) 315 | playNote(67, n + 0.25, n + 0.75) 316 | playNote(62, n + 0.25, n + 0.75) 317 | playNote(50, n + 0.25, n + 0.5) 318 | playNote(57, n + 0.5, n + 1.0) 319 | playNote(59, n + 0.75, n + 1.0) 320 | break 321 | case 40: // half-bar 41 322 | playNote(72, n + 0.0, n + 0.5) 323 | playNote(64, n + 0.0, n + 0.5) 324 | playNote(60, n + 0.0, n + 0.5) 325 | playNote(52, n + 0.0, n + 0.5) 326 | playNote(45, n + 0.0, n + 0.5) 327 | playNote(72, n + 0.5, n + 0.75) 328 | playNote(64, n + 0.5, n + 0.75) 329 | playNote(60, n + 0.5, n + 0.75) 330 | playNote(57, n + 0.5, n + 0.75) 331 | playNote(72, n + 0.75, n + 1.25) 332 | playNote(67, n + 0.75, n + 1.25) 333 | playNote(60, n + 0.75, n + 1.25) 334 | playNote(48, n + 0.75, n + 1.0) 335 | break 336 | case 41: // half-bar 42 337 | playNote(57, n + 0.0, n + 0.5) 338 | playNote(53, n + 0.0, n + 0.5) 339 | playNote(48, n + 0.0, n + 0.5) 340 | playNote(71, n + 0.25, n + 0.5) 341 | playNote(69, n + 0.5, n + 0.75) 342 | playNote(60, n + 0.5, n + 0.75) 343 | playNote(57, n + 0.5, n + 0.75) 344 | playNote(67, n + 0.75, n + 1.0) 345 | playNote(53, n + 0.75, n + 1.0) 346 | break 347 | case 42: // half-bar 43 348 | playNote(67, n + 0.0, n + 0.25) 349 | playNote(48, n + 0.0, n + 0.25) 350 | playNote(36, n + 0.0, n + 0.25) 351 | playNote(69, n + 0.25, n + 0.5) 352 | playNote(43, n + 0.25, n + 0.5) 353 | playNote(71, n + 0.5, n + 0.75) 354 | playNote(48, n + 0.5, n + 0.75) 355 | playNote(72, n + 0.75, n + 1.25) 356 | playNote(68, n + 0.75, n + 1.25) 357 | playNote(64, n + 0.75, n + 1.25) 358 | playNote(60, n + 0.75, n + 1.25) 359 | playNote(44, n + 0.75, n + 1.0) 360 | break 361 | case 43: // half-bar 44 362 | playNote(52, n + 0.0, n + 0.25) 363 | playNote(71, n + 0.25, n + 1.0) 364 | playNote(68, n + 0.25, n + 1.0) 365 | playNote(64, n + 0.25, n + 1.0) 366 | playNote(47, n + 0.25, n + 0.5) 367 | playNote(56, n + 0.5, n + 0.75) 368 | playNote(52, n + 0.75, n + 1.0) 369 | break 370 | case 44: // half-bar 45 371 | playNote(72, n + 0.0, n + 0.5) 372 | playNote(64, n + 0.0, n + 0.5) 373 | playNote(60, n + 0.0, n + 0.5) 374 | playNote(57, n + 0.0, n + 0.25) 375 | playNote(45, n + 0.0, n + 0.25) 376 | playNote(52, n + 0.25, n + 0.5) 377 | playNote(72, n + 0.5, n + 0.75) 378 | playNote(60, n + 0.5, n + 0.75) 379 | playNote(57, n + 0.5, n + 0.75) 380 | playNote(72, n + 0.75, n + 1.25) 381 | playNote(67, n + 0.75, n + 1.25) 382 | playNote(60, n + 0.75, n + 1.25) 383 | playNote(48, n + 0.75, n + 1.0) 384 | break 385 | case 45: // half-bar 46 386 | playNote(57, n + 0.0, n + 0.25) 387 | playNote(53, n + 0.0, n + 0.25) 388 | playNote(71, n + 0.25, n + 0.5) 389 | playNote(55, n + 0.25, n + 0.5) 390 | playNote(69, n + 0.5, n + 0.75) 391 | playNote(48, n + 0.5, n + 1.0) 392 | playNote(67, n + 0.75, n + 1.0) 393 | playNote(64, n + 0.75, n + 1.0) 394 | playNote(60, n + 0.75, n + 1.0) 395 | break 396 | case 46: // half-bar 47 397 | playNote(67, n + 0.0, n + 0.25) 398 | playNote(64, n + 0.0, n + 0.25) 399 | playNote(60, n + 0.0, n + 0.25) 400 | playNote(48, n + 0.0, n + 0.25) 401 | playNote(36, n + 0.0, n + 0.25) 402 | playNote(69, n + 0.25, n + 0.5) 403 | playNote(43, n + 0.25, n + 0.5) 404 | playNote(71, n + 0.5, n + 0.75) 405 | playNote(48, n + 0.5, n + 1.0) 406 | playNote(72, n + 0.75, n + 1.25) 407 | playNote(68, n + 0.75, n + 1.25) 408 | playNote(60, n + 0.75, n + 1.25) 409 | break 410 | case 47: // half-bar 48 411 | playNote(52, n + 0.0, n + 0.5) 412 | playNote(40, n + 0.0, n + 0.5) 413 | playNote(71, n + 0.25, n + 1.0) 414 | playNote(68, n + 0.25, n + 1.0) 415 | playNote(62, n + 0.25, n + 1.0) 416 | playNote(47, n + 0.5, n + 0.75) 417 | playNote(52, n + 0.75, n + 1.0) 418 | break 419 | case 48: // half-bar 49 420 | playNote(71, n + 0.0, n + 2.0) 421 | playNote(68, n + 0.0, n + 2.0) 422 | playNote(62, n + 0.0, n + 2.0) 423 | playNote(52, n + 0.0, n + 0.25) 424 | playNote(54, n + 0.25, n + 0.75) 425 | playNote(56, n + 0.75, n + 1.25) 426 | break 427 | case 49: // half-bar 50 428 | playNote(54, n + 0.25, n + 0.5) 429 | playNote(56, n + 0.5, n + 1.0) 430 | break 431 | case 50: // half-bar 51 432 | playNote(64, n + 0.0, n + 1.75) 433 | break 434 | case 51: // half-bar 52 435 | playNote(52, n + 0.75, n + 1.0) 436 | break 437 | } 438 | } 439 | 440 | let prevPart = -1 441 | 442 | function enqueue() { 443 | let bufferWanted = ac.currentTime - songStart + 4 444 | let queued = (prevPart + 1) * TEMPO_MUL 445 | 446 | if (queued > bufferWanted) return 447 | bufferWanted += 4 448 | 449 | while (queued < bufferWanted) { 450 | playPart(++prevPart) 451 | queued += TEMPO_MUL 452 | } 453 | } 454 | 455 | function playLoop() { 456 | songStart = ac.currentTime + 0.05 457 | enqueue() 458 | setInterval(enqueue, 999) 459 | } 460 | -------------------------------------------------------------------------------- /typescript/audio/reverbgen.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | if (typeof AudioContext == 'undefined') { // Safari 5 | window['AudioContext'] = (window as any)['webkitAudioContext'] 6 | window['OfflineAudioContext'] = (window as any)['webkitOfflineAudioContext'] 7 | } 8 | 9 | interface IReverbOptions { 10 | audioContext?: AudioContext 11 | sampleRate?: number 12 | channels?: number 13 | decay: number 14 | fadeIn: number 15 | lpFreqStart: number 16 | lpFreqEnd: number 17 | } 18 | 19 | type DoneFun = (buf: AudioBuffer) => void 20 | 21 | function randomInclusive(): number { 22 | return Math.floor(Math.random() * 0x20000000000000) / 0x1fffffffffffff 23 | } 24 | 25 | function getChannelData(buf: AudioBuffer) { 26 | const channels = [] 27 | for (let a = 0; a < buf.numberOfChannels; ++a) { 28 | channels[a] = buf.getChannelData(a) 29 | } 30 | return channels 31 | } 32 | 33 | function applyGradualLowpass(buf: AudioBuffer, lpFreqStart: number, lpFreqEnd: number, lpFreqEndAt: number, done: DoneFun) { 34 | if (!lpFreqStart) { 35 | done(buf) 36 | return 37 | } 38 | lpFreqStart = Math.min(lpFreqStart, 0.5 * buf.sampleRate) 39 | lpFreqEnd = Math.min(lpFreqEnd, 0.5 * buf.sampleRate) 40 | 41 | const channels = getChannelData(buf) 42 | const audioContext = new OfflineAudioContext(buf.numberOfChannels, 43 | channels[0].length, buf.sampleRate) 44 | const player = audioContext.createBufferSource() 45 | const filter = audioContext.createBiquadFilter() 46 | 47 | filter.type = 'lowpass' 48 | filter.Q.value = 0.0001 49 | filter.frequency.setValueAtTime(lpFreqStart, 0) 50 | filter.frequency.linearRampToValueAtTime(lpFreqEnd, lpFreqEndAt) 51 | filter.connect(audioContext.destination) 52 | 53 | player.buffer = buf 54 | player.connect(filter) 55 | player.start() 56 | 57 | audioContext.oncomplete = function (event) { 58 | done(event.renderedBuffer) 59 | } 60 | audioContext.startRendering() 61 | } 62 | 63 | function generateReverb(options: IReverbOptions, done: DoneFun) { 64 | const audioContext = options.audioContext || new AudioContext 65 | const sampleRate = options.sampleRate || audioContext.sampleRate 66 | const channels = options.channels || 2 67 | const length = 1.5 * options.decay 68 | const lengthFrames = Math.ceil(sampleRate * length) 69 | const fadeInFrames = Math.ceil(sampleRate * options.fadeIn) 70 | const decayFrames = Math.ceil(sampleRate * options.decay) 71 | const decayBase = Math.pow(0.001, 1 / decayFrames) 72 | const buf = audioContext.createBuffer(channels, lengthFrames, sampleRate) 73 | 74 | for (let a = 0; a < channels; ++a) { 75 | const chan = buf.getChannelData(a) 76 | for (let b = 0; b < lengthFrames; ++b) { 77 | chan[b] = (2 * randomInclusive() - 1) * Math.pow(decayBase, b) 78 | } 79 | for (let b = 0; b < fadeInFrames; ++b) { 80 | chan[b] *= b / fadeInFrames 81 | } 82 | } 83 | 84 | applyGradualLowpass(buf, options.lpFreqStart, options.lpFreqEnd, options.decay, done) 85 | } 86 | -------------------------------------------------------------------------------- /typescript/js13k2020.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | /// 5 | /// 6 | /// 7 | 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | -------------------------------------------------------------------------------- /typescript/levels/Level_02_TheWall.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class TheWall extends Level { 5 | wall: Wall 6 | 7 | constructor(startingPoint: NVec2, curtain = 0) { 8 | super(startingPoint, curtain) 9 | 10 | this.wall = new Wall(this, startingPoint.x + 256, (Settings.screenHeight - 256) * 0.5, 1, 9999) 11 | } 12 | 13 | getIndex() { 14 | return 1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typescript/levels/Level_03_Opening.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Opening extends Level { 5 | constructor(startingPoint: NVec2, curtain = 0) { 6 | super(startingPoint, curtain) 7 | 8 | new Wall(this, startingPoint.x + 256, (Settings.screenHeight - 256) * 0.5 - 140, 1, 9999) 9 | .rotate(EIGHTHPI) 10 | 11 | new Wall(this, startingPoint.x + 256, (Settings.screenHeight - 256) * 0.5 + 140, 1, 9999) 12 | .rotate(-EIGHTHPI) 13 | } 14 | 15 | getIndex() { 16 | return 2 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /typescript/levels/Level_04_Breach.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Breach extends TheWall { 5 | constructor(startingPoint: NVec2, curtain = 0) { 6 | super(startingPoint, curtain) 7 | 8 | const y0 = (Settings.screenHeight - 64 * 8) * 0.5 9 | 10 | for (let n = 0; n < 8; ++n) { 11 | if (n < 2 || n > 5) { 12 | new Box(this, startingPoint.x + 256 + 32, y0 + n * 64 + 32, 64, 0.5, 0.5) 13 | } 14 | } 15 | 16 | new Wall(this, startingPoint.x + 256, y0 - 256, 1, 9999) 17 | new Wall(this, startingPoint.x + 256, Settings.screenHeight - y0, 1, 9999) 18 | } 19 | 20 | getIndex() { 21 | return 3 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /typescript/levels/Level_05_Banned.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Banned extends TheWall { 5 | constructor(startingPoint: NVec2, curtain = 0) { 6 | super(startingPoint, curtain) 7 | 8 | this.duration = 196 9 | } 10 | 11 | updateTargeting(pos: IVec2) { 12 | this.reticle.lastPosition.setTo(pos) 13 | 14 | // Place the wall. 15 | register0.setSubtract(this.startingPoint, pos) 16 | if (register0.length() < 16) return 17 | 18 | const a = Math.atan2(register0.y, register0.x) 19 | const cos64 = 64 * Math.cos(a) 20 | const sin64 = 64 * Math.sin(a) 21 | 22 | register0.scalarMult(256 / register0.length()) 23 | register0.add(this.startingPoint) 24 | 25 | const v = this.wall.vertices 26 | let x = register0.x + 128 * Math.cos(a - HALFPI) 27 | let y = register0.y + 128 * Math.sin(a - HALFPI) 28 | 29 | v[0].set(x, y) 30 | v[1].set(x + cos64, y + sin64) 31 | 32 | x = register0.x + 128 * Math.cos(a + HALFPI) 33 | y = register0.y + 128 * Math.sin(a + HALFPI) 34 | 35 | v[2].set(x + cos64, y + sin64) 36 | v[3].set(x, y) 37 | } 38 | 39 | getIndex() { 40 | return 4 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /typescript/levels/Level_06_Reversal.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Reversal extends Level { 5 | constructor(startingPoint: NVec2, curtain = 0) { 6 | super(startingPoint, curtain) 7 | 8 | register0.set( 9 | (Settings.screenWidth - Settings.websiteWidth - startingPoint.x) * 0.5, 10 | -Settings.screenHeight * 0.125 11 | ) 12 | 13 | // Move the projectile. 14 | for (const vert of this.projectile.vertices) { 15 | vert.position.add(register0) 16 | vert.oldPosition.add(register0) 17 | } 18 | this.projectile.center.add(register0) 19 | 20 | // This is our new projectile. 21 | new Box(this, startingPoint.x, startingPoint.y, 64, 0.5, 4) 22 | } 23 | 24 | getIndex() { 25 | return 5 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /typescript/levels/Level_07_Moving.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Moving extends Level { 5 | constructor(startingPoint: NVec2, curtain = 0) { 6 | super(startingPoint, curtain) 7 | 8 | this.website = new MovingWebsite 9 | this.curtainPicture = FAILURE_MOVED_PICTURE 10 | } 11 | 12 | /** Solve constraints and collisions. */ 13 | solve() { 14 | super.solve() 15 | 16 | if (this.state === LevelState.WAITING || this.state === LevelState.FAILING || this.state === LevelState.WINNING) { 17 | this.website.update() 18 | } 19 | } 20 | 21 | getIndex() { 22 | return 6 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /typescript/levels/Level_08_Distancing.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Distancing extends Level { 5 | clone: UserAgent 6 | 7 | constructor(startingPoint: NVec2, curtain = 0) { 8 | super(startingPoint, curtain) 9 | 10 | this.clone = new InternetExplorer(this, 11 | Settings.screenWidth - Settings.websiteWidth * 0.5, 12 | Settings.screenHeight * 0.5) 13 | } 14 | 15 | /** Verlet integration loop. */ 16 | integrate() { 17 | do { 18 | if (this.state !== LevelState.WAITING) break 19 | 20 | register0.setSubtract(this.projectile.center, this.clone.center) 21 | 22 | // normalize 23 | const length = register0.length() 24 | if (length < 64) break 25 | register0.scalarMult(1 / length) 26 | 27 | // Move the clone. 28 | for (const vert of this.clone.vertices) { 29 | vert.position.add(register0) 30 | } 31 | } 32 | while (false) 33 | 34 | super.integrate() 35 | } 36 | 37 | getIndex() { 38 | return 7 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /typescript/levels/Level_09_Hopeless.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class Hopeless extends Level { 5 | static getUserAgent() { 6 | return InternetExplorer 7 | } 8 | 9 | constructor(startingPoint: NVec2, curtain = 0) { 10 | super(startingPoint, curtain) 11 | 12 | this.website = new NoWebsite 13 | this.duration = 196 14 | this.autoWin = true 15 | 16 | new WebsiteBox(this, 17 | Settings.screenWidth - Settings.websiteWidth - 1, 18 | (Settings.screenHeight - Settings.websiteHeight) * 0.5) 19 | } 20 | 21 | /** Verlet integration loop. */ 22 | integrate() { 23 | let gravity: number 24 | let fg: number 25 | 26 | if (this.state === LevelState.WAITING || this.state === LevelState.FAILING || this.state === LevelState.WINNING) { 27 | gravity = 0.9 28 | fg = 0.5 29 | } 30 | else { 31 | gravity = Settings.kGravity 32 | fg = Settings.kFrictionGround 33 | } 34 | 35 | for (let i = 0; i < this.vertices.length; ++i) { 36 | this.vertices[i].integrate(gravity, fg) 37 | } 38 | } 39 | 40 | getIndex() { 41 | return 8 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typescript/levels/Level_10_End.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | class End extends Level { 5 | static getUserAgent() { 6 | return Firefox 7 | } 8 | 9 | constructor(startingPoint: NVec2, curtain = 0) { 10 | super(startingPoint, curtain) 11 | 12 | this.website = new NoWebsite 13 | 14 | new InternetExplorer(this, 15 | Settings.screenWidth * 0.5, 16 | Settings.screenHeight * 0.5) 17 | 18 | new BigBrother(this, 19 | Settings.screenWidth - this.startingPoint.x, 20 | Settings.screenHeight * 0.5) 21 | 22 | for (let y = 1; y <= 3; ++y) { 23 | for (let x = 0; x < y; ++x) { 24 | new Box(this, 25 | Settings.screenWidth - 64 * x - 32 * (3 - y) - 33, 26 | Settings.screenHeight - 64 * (3 - y) - 33, 27 | 64, 0.5, 0.5) 28 | } 29 | } 30 | } 31 | 32 | /** Solve constraints and collisions. */ 33 | solve() { 34 | super.solve() 35 | 36 | if (this.state === LevelState.WAITING) { 37 | this.state = LevelState.INITIAL 38 | } 39 | } 40 | 41 | getIndex() { 42 | return 9 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /typescript/natlib/Canvas.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | function setSize($can: HTMLCanvasElement, can: CanvasRenderingContext2D, width: number, height: number) { 5 | if (window.devicePixelRatio > 1.44) { 6 | $can.height = 2 * height 7 | $can.width = 2 * width 8 | 9 | can.scale(2, 2) 10 | } 11 | else { 12 | $can.height = height 13 | $can.width = width 14 | } 15 | } 16 | 17 | const $canvas = $('.can') 18 | const canvas = $canvas.getContext('2d')! 19 | 20 | setSize($canvas, canvas, Settings.screenWidth, Settings.screenHeight) 21 | 22 | // #region Autosize canvas. 23 | const aspectRatio = 16 / 9 24 | 25 | let uiScale = 1 26 | 27 | let transformProperty: 'transform' | 'webkitTransform' = 'transform' 28 | if (!(transformProperty in $canvas.style)) { 29 | transformProperty = 'webkitTransform' 30 | } 31 | 32 | const hasVisualViewport = typeof visualViewport !== 'undefined' 33 | 34 | function handleResize() { 35 | let w = hasVisualViewport ? visualViewport.width : innerWidth 36 | let h = hasVisualViewport ? visualViewport.height : innerHeight 37 | 38 | if (w / h > aspectRatio) 39 | w = h * aspectRatio 40 | else 41 | h = w / aspectRatio 42 | 43 | uiScale = Settings.screenWidth / w 44 | 45 | const k = w / Settings.screenWidth 46 | 47 | $canvas.style[transformProperty] = `scale3d(${k},${k},1)` 48 | } 49 | 50 | addEventListener('resize', handleResize) 51 | addEventListener('orientationchange', handleResize) 52 | if (hasVisualViewport) visualViewport.addEventListener('resize', handleResize) 53 | // #endregion 54 | 55 | const systemFont = `16px -apple-system, 'Segoe UI', system-ui, Roboto, sans-serif` 56 | const systemFontHeading = systemFont.replace('16', 'bold 48') 57 | 58 | $canvas.addEventListener('contextmenu', event => { 59 | event.preventDefault() 60 | }) 61 | -------------------------------------------------------------------------------- /typescript/natlib/Mainloop.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | type LoopCallback = (t: number) => void 9 | 10 | /** A fixed-step loop. */ 11 | const startMainloop = (function () { 12 | // Update receives `T` seconds. 13 | let update: LoopCallback = _unused => undefined 14 | 15 | // Render receives `t` (0..1) for lerp. 16 | let render: LoopCallback = _unused => undefined 17 | 18 | /** Run the physics at 50 FPS, independent of rendering. */ 19 | const T = 0.02 20 | 21 | let then = -1 22 | let t = 0 23 | 24 | /** A fixed-step loop. */ 25 | function loop(now: number) { 26 | requestAnimationFrame(loop) 27 | 28 | if (then === -1) { 29 | then = now 30 | } 31 | t += (now - then) * 0.001 32 | then = now 33 | 34 | // Late updates are capped. 35 | let nUpdates = 2 36 | 37 | while (t > 0) { 38 | t -= T 39 | if (nUpdates > 0) { 40 | update(T) 41 | --nUpdates 42 | } 43 | } 44 | 45 | render(t / T + 1) 46 | } 47 | 48 | /** Start the loop. */ 49 | function startMainloop(updateFun: LoopCallback, renderFun: LoopCallback) { 50 | update = updateFun 51 | render = renderFun 52 | requestAnimationFrame(loop) 53 | } 54 | 55 | return startMainloop 56 | })() 57 | -------------------------------------------------------------------------------- /typescript/natlib/NBall.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** A ball. */ 9 | class NBall extends NBody { 10 | /** Create a new ball. */ 11 | constructor(scene: NScene, x: number, y: number, r: number, nVertices = 9, stiffness = 0.5, mass = 1) { 12 | super(scene, mass) 13 | 14 | const theta = TWOPI / nVertices 15 | 16 | // Create vertices. 17 | for (let i = 0; i < nVertices; ++i) { 18 | const a = theta * i 19 | new NVertex(this, x + r * Math.cos(a), y + r * Math.sin(a)) 20 | } 21 | 22 | // Create constraints. 23 | for (let i = 0; i < this.vertices.length - 1; ++i) { 24 | for (let j = i + 1; j < this.vertices.length; ++j) { 25 | new NConstraint(this, this.vertices[i], this.vertices[j], j === i + 1, stiffness) 26 | } 27 | } 28 | 29 | this.center.set(x, y) 30 | this.halfExtents.set(r, r) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /typescript/natlib/NBody.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** A physical body. */ 9 | class NBody { 10 | scene: NScene 11 | vertices: NVertex[] 12 | positions: NVec2[] 13 | constraints: NConstraint[] 14 | edges: NConstraint[] 15 | center: NVec2 16 | halfExtents: NVec2 17 | pMin: number 18 | pMax: number 19 | mass: number 20 | 21 | /** Create a new body. */ 22 | constructor(scene: NScene, mass = 1) { 23 | this.scene = scene 24 | this.vertices = [] 25 | this.positions = [] 26 | this.constraints = [] 27 | this.edges = [] 28 | this.center = new NVec2 29 | this.halfExtents = new NVec2 30 | this.pMin = 0 31 | this.pMax = 0 32 | this.mass = mass 33 | 34 | scene.bodies.push(this) 35 | } 36 | 37 | /** Compute the bounding box. */ 38 | boundingBox() { 39 | let p = this.positions[0] 40 | let xMin = p.x 41 | let xMax = p.x 42 | let yMin = p.y 43 | let yMax = p.y 44 | 45 | for (let i = 1; i < this.positions.length; ++i) { 46 | p = this.positions[i] 47 | 48 | if (p.x < xMin) xMin = p.x 49 | else if (p.x > xMax) xMax = p.x 50 | 51 | if (p.y < yMin) yMin = p.y 52 | else if (p.y > yMax) yMax = p.y 53 | } 54 | 55 | this.center.set((xMin + xMax) * 0.5, (yMin + yMax) * 0.5) 56 | this.halfExtents.set((xMax - xMin) * 0.5, (yMax - yMin) * 0.5) 57 | } 58 | 59 | /** Project the vertices onto the axis `a`. */ 60 | projectOn(a: NVec2) { 61 | let product = this.positions[0].dot(a) 62 | this.pMin = this.pMax = product 63 | 64 | for (let i = 1; i < this.positions.length; ++i) { 65 | product = this.positions[i].dot(a) 66 | 67 | if (product < this.pMin) this.pMin = product 68 | else if (product > this.pMax) this.pMax = product 69 | } 70 | } 71 | 72 | /** Paint the body. */ 73 | paint(canvas: CanvasRenderingContext2D, t: number) { 74 | // Interpolate vertices. 75 | for (const vert of this.vertices) vert.interpolate(t) 76 | 77 | // Paint non-edges. 78 | canvas.beginPath() 79 | 80 | for (const cons of this.constraints) { 81 | if (cons.edge) continue 82 | canvas.moveTo(cons.v0.interpolated.x, cons.v0.interpolated.y) 83 | canvas.lineTo(cons.v1.interpolated.x, cons.v1.interpolated.y) 84 | } 85 | 86 | canvas.strokeStyle = '#ffac7f' 87 | canvas.stroke() 88 | 89 | // Paint edges. 90 | canvas.beginPath() 91 | 92 | for (const vert of this.vertices) { 93 | canvas.lineTo(vert.interpolated.x, vert.interpolated.y) 94 | } 95 | 96 | canvas.closePath() 97 | 98 | canvas.strokeStyle = '#99c2db' 99 | canvas.stroke() 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /typescript/natlib/NConstraint.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** A Verlet integration constraint. */ 9 | class NConstraint { 10 | parent: NBody 11 | v0: NVertex 12 | v1: NVertex 13 | p0: NVec2 14 | p1: NVec2 15 | dist: number 16 | edge: boolean 17 | stiffness: number 18 | 19 | /** Create a new constraint. */ 20 | constructor(parent: NBody, v0: NVertex, v1: NVertex, edge = false, stiffness = 1) { 21 | this.parent = parent 22 | this.v0 = v0 23 | this.v1 = v1 24 | this.p0 = v0.position 25 | this.p1 = v1.position 26 | this.dist = this.p0.distanceSquared(this.p1) 27 | this.edge = edge 28 | this.stiffness = stiffness 29 | 30 | if (!this.dist) throw Error('Overlapping vertices.') 31 | 32 | parent.constraints.push(this) 33 | if (edge) parent.edges.push(this) 34 | parent.scene.constraints.push(this) 35 | } 36 | 37 | /** Solve this constraint. */ 38 | solve() { 39 | register0.setSubtract(this.p0, this.p1) 40 | 41 | // using square root approximation 42 | const a = this.dist / (register0.dot(register0) + this.dist) - 0.5 43 | register0.scalarMult(a * this.stiffness) 44 | 45 | this.p0.add(register0) 46 | this.p1.subtract(register0) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /typescript/natlib/NScene.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** A scene. */ 9 | class NScene { 10 | vertices: NVertex[] 11 | constraints: NConstraint[] 12 | bodies: NBody[] 13 | 14 | /** Create a new scene. */ 15 | constructor() { 16 | this.vertices = [] 17 | this.constraints = [] 18 | this.bodies = [] 19 | } 20 | 21 | /** Verlet integration loop. */ 22 | integrate() { 23 | for (let i = 0; i < this.vertices.length; ++i) { 24 | this.vertices[i].integrate() 25 | } 26 | } 27 | 28 | /** Solve constraints and collisions. */ 29 | solve() { 30 | for (let n = 0; n < Settings.kNumIterations; ++n) { 31 | // Solve constraints. 32 | for (const c of this.constraints) { 33 | c.solve() 34 | } 35 | 36 | // Recalculate the bounding boxes. 37 | for (const b of this.bodies) { 38 | b.boundingBox() 39 | } 40 | 41 | // Collision detection. 42 | for (let i = 0; i < this.bodies.length - 1; ++i) { 43 | for (let j = i + 1; j < this.bodies.length; ++j) { 44 | satResolve(this.bodies[i], this.bodies[j]) 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /typescript/natlib/NStaticVertex.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** A static vertex. */ 9 | class NStaticVertex extends NVertex { 10 | x: number 11 | y: number 12 | 13 | /** Create a new static vertex. */ 14 | constructor(parent: NBody, x: number, y: number) { 15 | super(parent, x, y) 16 | 17 | this.x = x 18 | this.y = y 19 | } 20 | 21 | /** Set the `x` and `y` components of this vertex. */ 22 | set(x: number, y: number) { 23 | this.x = x 24 | this.y = y 25 | } 26 | 27 | /** Verlet integration override. */ 28 | integrate() { 29 | this.position.set(this.x, this.y) 30 | this.oldPosition.set(this.x, this.y) 31 | } 32 | 33 | /** Interpolate this vertex. */ 34 | interpolate(_t: number) { 35 | this.interpolated.set(this.x, this.y) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /typescript/natlib/NVec2.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | interface IVec2 { 9 | x: number 10 | y: number 11 | } 12 | 13 | /** A 2D vector. */ 14 | class NVec2 implements IVec2 { 15 | x: number 16 | y: number 17 | 18 | /** Create a new vector. */ 19 | constructor(x = 0, y = 0) { 20 | this.x = x 21 | this.y = y 22 | } 23 | 24 | /** Set the `x` and `y` components of this vector. */ 25 | set(x: number, y: number) { 26 | this.x = x 27 | this.y = y 28 | } 29 | 30 | /** Copy the values of the other vector's `x` and `y` properties to this vector. */ 31 | setTo(other: IVec2) { 32 | this.x = other.x 33 | this.y = other.y 34 | } 35 | 36 | /** Compute the Euclidean length of this vector. */ 37 | length(): number { 38 | return Math.sqrt(this.x * this.x + this.y * this.y) 39 | } 40 | 41 | /** Compute the squared distance from this vector to the other one. */ 42 | distanceSquared(other: IVec2): number { 43 | const x = this.x - other.x 44 | const y = this.y - other.y 45 | return x * x + y * y 46 | } 47 | 48 | /** Add the other vector to this one. */ 49 | add(other: IVec2) { 50 | this.x += other.x 51 | this.y += other.y 52 | } 53 | 54 | /** Subtract the other vector from this one. */ 55 | subtract(other: IVec2) { 56 | this.x -= other.x 57 | this.y -= other.y 58 | } 59 | 60 | /** Set this vector to `a` − `b`. */ 61 | setSubtract(a: IVec2, b: IVec2) { 62 | this.x = a.x - b.x 63 | this.y = a.y - b.y 64 | } 65 | 66 | /** Compute the dot product of this vector and the other one. */ 67 | dot(other: IVec2): number { 68 | return this.x * other.x + this.y * other.y 69 | } 70 | 71 | /** Multiply this vector by the scalar `a`. */ 72 | scalarMult(a: number) { 73 | this.x *= a 74 | this.y *= a 75 | } 76 | 77 | /** Set this vector to the multiple of the other vector by the scalar `a`. */ 78 | setScalarMult(other: IVec2, a: number) { 79 | this.x = other.x * a 80 | this.y = other.y * a 81 | } 82 | 83 | /** Set this vector to the normal to the edge `ab`. */ 84 | setNormal(a: IVec2, b: IVec2) { 85 | // perpendicular 86 | this.x = a.y - b.y 87 | this.y = b.x - a.x 88 | 89 | // normalize 90 | const length = this.length() 91 | if (length < Number.MIN_VALUE) return 92 | this.scalarMult(1 / length) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /typescript/natlib/NVertex.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** A Verlet integration vertex. */ 9 | class NVertex { 10 | parent: NBody 11 | position: NVec2 12 | oldPosition: NVec2 13 | interpolated: NVec2 14 | 15 | /** Create a new vertex. */ 16 | constructor(parent: NBody, x: number, y: number) { 17 | this.parent = parent 18 | this.position = new NVec2(x, y) 19 | this.oldPosition = new NVec2(x, y) 20 | this.interpolated = new NVec2 21 | 22 | parent.vertices.push(this) 23 | parent.positions.push(this.position) 24 | parent.scene.vertices.push(this) 25 | } 26 | 27 | /** Verlet integration. */ 28 | integrate(gravity = Settings.kGravity, fg = Settings.kFrictionGround) { 29 | const pos = this.position 30 | const old = this.oldPosition 31 | const x = pos.x 32 | const y = pos.y 33 | 34 | pos.x += (pos.x - old.x) * Settings.kViscosity 35 | pos.y += (pos.y - old.y) * Settings.kViscosity + gravity 36 | 37 | old.set(x, y) 38 | 39 | // screen limits 40 | if (pos.y < 0) pos.y = 0 41 | else if (pos.y >= Settings.screenHeight) { 42 | pos.x -= (pos.x - old.x) * fg 43 | pos.y = Settings.screenHeight - 1 44 | } 45 | 46 | if (pos.x < 0) pos.x = 0 47 | else if (pos.x >= Settings.screenWidth) { 48 | pos.x = Settings.screenWidth - 1 49 | } 50 | } 51 | 52 | /** Interpolate this vertex. */ 53 | interpolate(t: number) { 54 | this.interpolated.set( 55 | lerp(this.oldPosition.x, this.position.x, t), 56 | lerp(this.oldPosition.y, this.position.y, t) 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /typescript/natlib/Pointer.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | interface IPointer extends IVec2 { 5 | dragging: boolean 6 | x: number 7 | y: number 8 | vertex?: NVertex 9 | } 10 | 11 | /** The pointer. */ 12 | const pointer: IPointer = { 13 | dragging: false, 14 | x: 0, 15 | y: 0, 16 | } 17 | 18 | /** Set the pointer position. */ 19 | function setPointerPosition(event: MouseEvent | Touch) { 20 | const r = $canvas.getBoundingClientRect() 21 | pointer.x = (event.clientX - r.left) * uiScale 22 | pointer.y = (event.clientY - r.top) * uiScale 23 | } 24 | 25 | /** Mouse events. */ 26 | document.addEventListener('mousedown', event => { 27 | event.preventDefault() 28 | 29 | pointer.dragging = true 30 | setPointerPosition(event) 31 | }) 32 | 33 | document.addEventListener('mousemove', event => { 34 | event.preventDefault() 35 | 36 | setPointerPosition(event) 37 | }) 38 | 39 | document.addEventListener('mouseup', event => { 40 | event.preventDefault() 41 | 42 | pointer.dragging = false 43 | pointer.vertex = undefined 44 | 45 | if (!audioInitialized) { 46 | initializeAudio() 47 | } 48 | }) 49 | 50 | /** Touch events. */ 51 | document.addEventListener('touchstart', event => { 52 | event.preventDefault() 53 | 54 | pointer.dragging = true 55 | setPointerPosition(event.targetTouches[0]) 56 | }) 57 | 58 | document.addEventListener('touchmove', event => { 59 | event.preventDefault() 60 | 61 | setPointerPosition(event.targetTouches[0]) 62 | }) 63 | 64 | document.addEventListener('touchend', event => { 65 | event.preventDefault() 66 | 67 | pointer.dragging = false 68 | pointer.vertex = undefined 69 | 70 | if (!audioInitialized) { 71 | initializeAudio() 72 | } 73 | }) 74 | 75 | document.addEventListener('touchcancel', event => { 76 | event.preventDefault() 77 | 78 | pointer.dragging = false 79 | pointer.vertex = undefined 80 | }) 81 | -------------------------------------------------------------------------------- /typescript/natlib/Prelude.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const enum Settings { 5 | kFriction = 0, 6 | kFrictionGround = 0, 7 | kGravity = 0, 8 | kNumIterations = 10, 9 | kViscosity = 1, 10 | screenHeight = 540, 11 | screenWidth = 960, 12 | // 13 | displaySize = 1102, // Math.ceil(Math.sqrt(960 ** 2 + 540 ** 2)) 14 | reticleStiffness = 0.1, 15 | targetCaptureDist = 64, 16 | targetReleaseDist = 256, 17 | waitCurtain = 24, 18 | waitLevel = 144, 19 | waitNextLevel = 69, 20 | websiteHeight = 196, 21 | websiteWidth = 110, 22 | websitePicHeight = 64, 23 | websitePicWidth = 88, 24 | } 25 | 26 | const register0 = new NVec2 27 | const register1 = new NVec2 28 | 29 | // Play a sound. 30 | function sound(snd: Snd) { 31 | try { 32 | if (snd.buf === null) { 33 | snd.buf = ac.createBuffer(1, snd.raw.length, zzfxR) 34 | snd.buf.getChannelData(0).set(snd.raw) 35 | } 36 | 37 | const bufs = ac.createBufferSource() 38 | bufs.buffer = snd.buf 39 | bufs.connect(ac.destination) 40 | bufs.start() 41 | } 42 | catch (err) { 43 | } 44 | } 45 | 46 | type Snd = { 47 | raw: number[] 48 | buf: AudioBuffer | null 49 | } 50 | 51 | const sndLaunch: Snd = { 52 | // zzfxMicro.apply(null, [,,398,.02,.06,.4,2,0,2.4,,,,,,,,.03,.79,.01]) 53 | raw: zzfxMicro.apply(null, [,,132,,,.46,,.11,9.1,,,,,.1,,,.04,.56,.05]), 54 | buf: null, 55 | } 56 | 57 | const sndFail: Snd = { 58 | raw: zzfxMicro.apply(null, [,,382,,,.48,2,.35,-0.6,,,,,,,,.2,.72,.09]), 59 | buf: null, 60 | } 61 | 62 | const sndWin: Snd = { 63 | // zzfxMicro.apply(null, [,,345,.01,.17,.87,2,1.05,.2,,67,.03,.02,,-0.2,,,.79,,.04]) 64 | raw: zzfxMicro.apply(null, [,,345,.01,.17,.87,1,1.05,.2,,67,.03,.02,,-0.2,,,.79,,.04]), 65 | buf: null, 66 | } 67 | 68 | let audioInitialized = false 69 | 70 | // Initialize audio. 71 | function initializeAudio() { 72 | try { 73 | audioInit().then(playLoop) 74 | } 75 | catch (err) { 76 | } 77 | 78 | audioInitialized = true 79 | } 80 | -------------------------------------------------------------------------------- /typescript/natlib/SAT.ts: -------------------------------------------------------------------------------- 1 | /* This file is part of natlib. 2 | * natlib, a library for games, is planned to release in early 2021. 3 | * https://github.com/mvasilkov/natlib 4 | */ 5 | 'use strict' 6 | /// 7 | 8 | /** Separating Axis Theorem collision testing and resolution. */ 9 | const satResolve = (function () { 10 | const a = new NVec2 11 | let cDist: number 12 | let cEdge: NConstraint 13 | let cVert: NVertex 14 | 15 | /** Projected distance function. */ 16 | function pDistance(b0: NBody, b1: NBody, edge: NConstraint): number { 17 | // Compute the normal to this edge. 18 | register0.setNormal(edge.p0, edge.p1) 19 | 20 | // Project both bodies onto the normal. 21 | b0.projectOn(register0) 22 | b1.projectOn(register0) 23 | 24 | // Compute the distance between the two intervals. 25 | return b0.pMin < b1.pMin ? b1.pMin - b0.pMax : b0.pMin - b1.pMax 26 | } 27 | 28 | /** Separating Axis Theorem collision detection. */ 29 | function sat(b0: NBody, b1: NBody) { 30 | const b0EdgesLength = b0.edges.length 31 | const b1EdgesLength = b1.edges.length 32 | if (b0EdgesLength === 0 || b1EdgesLength === 0) return 33 | 34 | // aabb overlap test 35 | if (Math.abs(b1.center.x - b0.center.x) >= b0.halfExtents.x + b1.halfExtents.x || 36 | Math.abs(b1.center.y - b0.center.y) >= b0.halfExtents.y + b1.halfExtents.y) 37 | // no aabb overlap 38 | return 39 | 40 | cDist = pDistance(b0, b1, cEdge = b0.edges[0]) 41 | a.setTo(register0) 42 | 43 | for (let i = 1; i < b0EdgesLength; ++i) { 44 | const edge = b0.edges[i] 45 | // begin copypasta 46 | const dist = pDistance(b0, b1, edge) 47 | // If the intervals don't overlap, there is no collision. 48 | if (dist > 0) return 49 | if (dist > cDist) { 50 | cDist = dist 51 | cEdge = edge 52 | a.setTo(register0) 53 | } 54 | // end copypasta 55 | } 56 | 57 | for (let i = 0; i < b1EdgesLength; ++i) { 58 | const edge = b1.edges[i] 59 | // begin copypasta 60 | const dist = pDistance(b0, b1, edge) 61 | // If the intervals don't overlap, there is no collision. 62 | if (dist > 0) return 63 | if (dist > cDist) { 64 | cDist = dist 65 | cEdge = edge 66 | a.setTo(register0) 67 | } 68 | // end copypasta 69 | } 70 | 71 | // There is no separating axis. 72 | // Ensure collision edge in `b1` and collision vertex in `b0`. 73 | if (cEdge.parent !== b1) { 74 | const t = b0 75 | b0 = b1 76 | b1 = t 77 | } 78 | 79 | // Ensure that the collision normal is pointing at `b1`. 80 | register0.setSubtract(b0.center, b1.center) 81 | if (register0.dot(a) < 0) a.scalarMult(-1) 82 | 83 | // Find the collision vertex. 84 | register0.setSubtract(b0.positions[0], b1.center) 85 | let distMin = a.dot(register0) 86 | cVert = b0.vertices[0] 87 | 88 | for (let i = 1; i < b0.positions.length; ++i) { 89 | register0.setSubtract(b0.positions[i], b1.center) 90 | const dist = a.dot(register0) 91 | if (dist < distMin) { 92 | distMin = dist 93 | cVert = b0.vertices[i] 94 | } 95 | } 96 | 97 | // Resolve the collision. 98 | resolve() 99 | } 100 | 101 | /** Collision resolution. */ 102 | function resolve() { 103 | const pos0 = cEdge.p0 104 | const pos1 = cEdge.p1 105 | const old0 = cEdge.v0.oldPosition 106 | const old1 = cEdge.v1.oldPosition 107 | const pos = cVert.position 108 | const old = cVert.oldPosition 109 | 110 | // response vector 111 | register0.setScalarMult(a, -cDist) 112 | 113 | // Find the position of the collision vertex on the edge. 114 | register1.setSubtract(pos1, pos0) 115 | const t = register1.x === 0 && register1.y === 0 ? 0.5 : 116 | Math.abs(register1.x) > Math.abs(register1.y) ? 117 | (pos.x - register0.x - pos0.x) / register1.x : 118 | (pos.y - register0.y - pos0.y) / register1.y 119 | 120 | // Mass coefficients. 121 | let c0 = cVert.parent.mass 122 | let c1 = cEdge.parent.mass 123 | const c = c0 + c1 124 | c0 /= c * 2 125 | c1 /= c 126 | 127 | const k = c0 / (t * t + (1 - t) * (1 - t)) 128 | const k0 = (1 - t) * k 129 | const k1 = t * k 130 | 131 | // apply the collision response 132 | pos0.x -= register0.x * k0 133 | pos0.y -= register0.y * k0 134 | pos1.x -= register0.x * k1 135 | pos1.y -= register0.y * k1 136 | 137 | pos.x += register0.x * c1 138 | pos.y += register0.y * c1 139 | 140 | // collision friction 141 | if (Settings.kFriction) { 142 | // compute relative velocity 143 | register0.set( 144 | pos.x - old.x - (pos0.x + pos1.x - old0.x - old1.x) * 0.5, 145 | pos.y - old.y - (pos0.y + pos1.y - old0.y - old1.y) * 0.5 146 | ) 147 | 148 | // project the relative velocity onto tangent 149 | register1.set(-a.y, a.x) 150 | register0.setScalarMult(register1, register0.dot(register1)) 151 | 152 | // apply tangent friction 153 | old0.x -= register0.x * Settings.kFriction * k0 154 | old0.y -= register0.y * Settings.kFriction * k0 155 | old1.x -= register0.x * Settings.kFriction * k1 156 | old1.y -= register0.y * Settings.kFriction * k1 157 | 158 | old.x += register0.x * Settings.kFriction * c1 159 | old.y += register0.y * Settings.kFriction * c1 160 | } 161 | } 162 | 163 | return sat 164 | })() 165 | -------------------------------------------------------------------------------- /typescript/natlib/Utils.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /// 3 | 4 | const TWOPI = 2 * Math.PI 5 | const HALFPI = 0.5 * Math.PI 6 | const FOURTHPI = 0.25 * Math.PI 7 | const EIGHTHPI = 0.125 * Math.PI 8 | 9 | /** Linear interpolation. */ 10 | function lerp(a: number, b: number, t: number): number { 11 | return a * (1 - t) + b * t 12 | } 13 | 14 | /** Quadratic easing. */ 15 | function easeInQuad(t: number) { 16 | return t * t 17 | } 18 | 19 | function easeOutQuad(t: number) { 20 | return t * (2 - t) 21 | } 22 | 23 | function easeInOutQuad(t: number) { 24 | return t < 0.5 ? 25 | 2 * t * t : 26 | 2 * t * (2 - t) - 1 27 | } 28 | 29 | function inverseRescale(a: number, start0: number, end0: number, start1: number, end1: number) { 30 | return (1 - (a - start0) / (end0 - start0)) * (end1 - start1) + start1 31 | } 32 | 33 | /** Get one element by class name. */ 34 | function $(selector: string) { 35 | return document.querySelector(selector)! 36 | } 37 | 38 | /** Get elements by class name. */ 39 | function $$(selector: string) { 40 | return document.querySelectorAll(selector) 41 | } 42 | 43 | /** Make it so that the passed coords are in the (0, 0) – (512, 512) range. */ 44 | function enclose(x0: number, y0: number, x1: number, y1: number) { 45 | canvas.translate(x0, y0) 46 | canvas.scale((x1 - x0) / 512, (y1 - y0) / 512) 47 | } 48 | 49 | /** A rendering function. */ 50 | type RenderFun = (canvas: CanvasRenderingContext2D) => void 51 | 52 | /** Render a static picture on canvas. */ 53 | function prerender(width: number, height: number, render: RenderFun): HTMLCanvasElement { 54 | const $can = document.createElement('canvas') 55 | const can = $can.getContext('2d')! 56 | 57 | setSize($can, can, width, height) 58 | render(can) 59 | 60 | return $can 61 | } 62 | -------------------------------------------------------------------------------- /typescript/natlib/natlib.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | -------------------------------------------------------------------------------- /typescript/webmonetization.d.ts: -------------------------------------------------------------------------------- 1 | interface BaseMonetizationEventDetail { 2 | paymentPointer: string 3 | requestId: string 4 | } 5 | 6 | export interface MonetizationPendingEvent extends CustomEvent { 7 | type: 'monetizationpending' 8 | } 9 | 10 | export interface MonetizationStartEvent extends CustomEvent { 11 | type: 'monetizationstart' 12 | } 13 | 14 | interface MonetizationStopEventDetail extends BaseMonetizationEventDetail { 15 | finalized: boolean 16 | } 17 | 18 | export interface MonetizationStopEvent extends CustomEvent { 19 | type: 'monetizationstop' 20 | } 21 | 22 | interface MonetizationProgressEventDetail extends BaseMonetizationEventDetail { 23 | amount: string 24 | assetCode: string 25 | assetScale: number 26 | } 27 | 28 | export interface MonetizationProgressEvent extends CustomEvent { 29 | type: 'monetizationprogress' 30 | } 31 | 32 | export interface MonetizationEventMap { 33 | monetizationpending: MonetizationPendingEvent 34 | monetizationstart: MonetizationStartEvent 35 | monetizationstop: MonetizationStopEvent 36 | monetizationprogress: MonetizationProgressEvent 37 | } 38 | 39 | export type MonetizationEvent = MonetizationEventMap[keyof MonetizationEventMap] 40 | 41 | export type MonetizationState = 'stopped' | 'pending' | 'started' 42 | 43 | type EventListener = (this: T, evt: E) => any 44 | 45 | interface EventListenerObject { 46 | handleEvent(this: T, evt: E): void 47 | } 48 | 49 | type EventListenerOrListenerObject = 50 | | EventListener 51 | | EventListenerObject 52 | 53 | // Note: The Coil extension uses a
instead of an EventTarget 54 | export interface Monetization extends EventTarget { 55 | state: MonetizationState 56 | 57 | addEventListener( 58 | type: K, 59 | listener: EventListenerOrListenerObject | null, 60 | options?: boolean | AddEventListenerOptions 61 | ): void 62 | 63 | removeEventListener( 64 | type: K, 65 | listener: EventListenerOrListenerObject | null, 66 | options?: boolean | EventListenerOptions 67 | ): void 68 | } 69 | 70 | declare global { 71 | interface Document { 72 | monetization?: Monetization 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /typescript/zzfx.d.ts: -------------------------------------------------------------------------------- 1 | // sample rate 2 | declare let zzfxR: number 3 | 4 | // volume 5 | declare let zzfxV: number 6 | 7 | // audio context 8 | declare let zzfxX: AudioContext 9 | 10 | // play sound 11 | declare function zzfxMicro(...args: (number | undefined)[]): number[] 12 | --------------------------------------------------------------------------------