Removed Security issue

This commit is contained in:
Lucas Kent 2022-11-24 11:30:45 -05:00
parent a5a3ff0b66
commit 3e5d0aeee3
63 changed files with 0 additions and 7208 deletions

View File

@ -1,3 +0,0 @@
.DS_Store
npm-debug.log
node_modules

View File

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017 maxwellito
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,541 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 250 120" width="250" height="120" enable-background="new 0 0 250 120" xml:space="preserve">
<!-- Playground -->
<!-- ################################################ -->
<!-- Fixed dots -->
<g fill="#fff">
<circle cx="15" cy="15" rel="0" r="2"></circle>
<circle cx="50" cy="15" rel="1" r="2"></circle>
<circle cx="85" cy="15" rel="2" r="2"></circle>
<circle cx="15" cy="50" rel="3" r="2"></circle>
<circle cx="50" cy="50" rel="4" r="2"></circle>
<circle cx="85" cy="50" rel="5" r="2"></circle>
<circle cx="15" cy="85" rel="6" r="2"></circle>
<circle cx="50" cy="85" rel="7" r="2"></circle>
<circle cx="85" cy="85" rel="8" r="2"></circle>
</g>
<!-- Big Dots (aka flash dots, the light flash while connecting a dot) -->
<g fill="#fff">
<circle cx="15" cy="15" class="bigdot bigdot-1" r="9"></circle>
<circle cx="50" cy="15" class="bigdot bigdot-2" r="9"></circle>
<circle cx="85" cy="15" class="bigdot bigdot-3" r="9"></circle>
<circle cx="15" cy="50" class="bigdot bigdot-4" r="9"></circle>
<circle cx="50" cy="50" class="bigdot bigdot-5" r="9"></circle>
<circle cx="85" cy="50" class="bigdot bigdot-6" r="9"></circle>
<circle cx="15" cy="85" class="bigdot bigdot-7" r="9"></circle>
<circle cx="50" cy="85" class="bigdot bigdot-8" r="9"></circle>
<circle cx="85" cy="85" class="bigdot bigdot-9" r="9"></circle>
</g>
<!-- The 4 different attemps -->
<g stroke-width="2" stroke-linecap="round" stroke="#ffffff" class="attempt-1">
<line class="attempt-1-1 line-1" x1="15" y1="15" x2="50" y2="15"></line>
<line class="attempt-1-2 line-1" x1="50" y1="15" x2="85" y2="15"></line>
<line class="attempt-1-3 line-1" x1="85" y1="15" x2="85" y2="50"></line>
</g>
<g stroke-width="2" stroke-linecap="round" stroke="#ffffff" class="attempt-2">
<line class="attempt-2-1 line-1" x1="15" y1="15" x2="15" y2="50"></line>
<line class="attempt-2-2 line-1" x1="15" y1="50" x2="15" y2="85"></line>
<line class="attempt-2-3 line-1" x1="15" y1="85" x2="50" y2="85"></line>
</g>
<g stroke-width="2" stroke-linecap="round" stroke="#ffffff" class="attempt-3">
<line class="attempt-3-1 line-1" x1="15" y1="85" x2="50" y2="85"></line>
<line class="attempt-3-2 line-1" x1="50" y1="85" x2="85" y2="85"></line>
<line class="attempt-3-3 line-1" x1="85" y1="85" x2="85" y2="50"></line>
</g>
<g stroke-width="2" stroke-linecap="round" stroke="#ffffff" class="attempt-4">
<line class="attempt-4-1 line-4" x1="15" y1="15" x2="85" y2="50"></line>
<line class="attempt-4-2 line-4" x1="85" y1="50" x2="15" y2="85"></line>
<line class="attempt-4-3 line-4" x1="15" y1="85" x2="50" y2="15"></line>
</g>
<!-- Result pos dots (the one on the top of the pattern to understand the importance of the position in the pattern) -->
<g stroke-width="2">
<circle class="result-pos result-pos-1" cx="15" cy="15" r="6.5"></circle>
<circle class="result-pos result-pos-2" cx="85" cy="50" r="6.5"></circle>
<circle class="result-pos result-pos-3" cx="15" cy="85" r="6.5"></circle>
<circle class="result-pos result-pos-4" cx="50" cy="15" r="6.5"></circle>
</g>
<!-- Result feedback (the result dots under the pattern) -->
<g stroke-width="2">
<circle class="result-dot result-dot-1" cx="20" cy="110" r="6.5"></circle>
<circle class="result-dot result-dot-2" cx="40" cy="110" r="6.5"></circle>
<circle class="result-dot result-dot-3" cx="60" cy="110" r="6.5"></circle>
<circle class="result-dot result-dot-4" cx="80" cy="110" r="6.5"></circle>
</g>
<!-- Delimiter -->
<!-- ################################################ -->
<line x1="125" y1="20" x2="125" y2="100" stroke="#fff" stroke-width="1" stroke-dasharray="1,1"></line>
<!-- Model -->
<!-- ################################################ -->
<!-- Fixed dots -->
<g fill="#fff">
<circle cx="165" cy="15" r="2"></circle>
<circle cx="200" cy="15" r="2"></circle>
<circle cx="235" cy="15" r="2"></circle>
<circle cx="165" cy="50" r="2"></circle>
<circle cx="200" cy="50" r="2"></circle>
<circle cx="235" cy="50" r="2"></circle>
<circle cx="165" cy="85" r="2"></circle>
<circle cx="200" cy="85" r="2"></circle>
<circle cx="235" cy="85" r="2"></circle>
</g>
<!-- The pattern model -->
<g stroke-width="6" stroke-linecap="round">
<line class="model-1 line-4" x1="165" y1="15" x2="235" y2="50" stroke="#666666"></line>
<line class="model-2 line-4" x1="235" y1="50" x2="165" y2="85" stroke="#b3b3b3"></line>
<line class="model-3 line-4" x1="165" y1="85" x2="200" y2="15" stroke="#ffffff"></line>
</g>
<!-- Result pos dots -->
<g stroke-width="2">
<circle class="result-pos result-pos-1" cx="165" cy="15" r="6.5"></circle>
<circle class="result-pos result-pos-2" cx="235" cy="50" r="6.5"></circle>
<circle class="result-pos result-pos-3" cx="165" cy="85" r="6.5"></circle>
<circle class="result-pos result-pos-4" cx="200" cy="15" r="6.5"></circle>
</g>
<!-- The little lock (not sure if this icon is relevant enough :-S) -->
<rect x="194" y="106" width="12" height="10" stroke="#fff" stroke-width="2" fill="none"></rect>
<path d="M195,105A5,5 0,1,1 205,105" stroke="#fff" stroke-width="2" fill="none"></path>
<style>
/* Attempts pattern (lines) **************************/
.line-1 {stroke-dasharray: 35 35;}
.line-4 {stroke-dasharray: 80 80;}
.attempt-1-1 { animation: attempt-1-1 30s infinite; }
.attempt-1-2 { animation: attempt-1-2 30s infinite; }
.attempt-1-3 { animation: attempt-1-3 30s infinite; }
.attempt-2-1 { animation: attempt-2-1 30s infinite; }
.attempt-2-2 { animation: attempt-2-2 30s infinite; }
.attempt-2-3 { animation: attempt-2-3 30s infinite; }
.attempt-3-1 { animation: attempt-3-1 30s infinite; }
.attempt-3-2 { animation: attempt-3-2 30s infinite; }
.attempt-3-3 { animation: attempt-3-3 30s infinite; }
.attempt-4-1 { animation: attempt-4-1 30s infinite; }
.attempt-4-2 { animation: attempt-4-2 30s infinite; }
.attempt-4-3 { animation: attempt-4-3 30s infinite; }
/* Attempt n.1 */
@keyframes attempt-1-1 {
0% {stroke-dashoffset: 35;}
1% {stroke-dashoffset: 35;}
2% {stroke-dashoffset: 0;}
23.999% {stroke-dashoffset: 0;}
24% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
@keyframes attempt-1-2 {
0% {stroke-dashoffset: 35;}
2% {stroke-dashoffset: 35;}
3% {stroke-dashoffset: 0;}
23.999% {stroke-dashoffset: 0;}
24% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
@keyframes attempt-1-3 {
0% {stroke-dashoffset: 35;}
3% {stroke-dashoffset: 35;}
4% {stroke-dashoffset: 0;}
23.999% {stroke-dashoffset: 0;}
24% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
/* Attempt n.2 */
@keyframes attempt-2-1 {
0% {stroke-dashoffset: 35;}
26% {stroke-dashoffset: 35;}
27% {stroke-dashoffset: 0;}
48.999% {stroke-dashoffset: 0;}
49% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
@keyframes attempt-2-2 {
0% {stroke-dashoffset: 35;}
27% {stroke-dashoffset: 35;}
28% {stroke-dashoffset: 0;}
48.999% {stroke-dashoffset: 0;}
49% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
@keyframes attempt-2-3 {
0% {stroke-dashoffset: 35;}
28% {stroke-dashoffset: 35;}
29% {stroke-dashoffset: 0;}
48.999% {stroke-dashoffset: 0;}
49% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
/* Attempt n.3 */
@keyframes attempt-3-1 {
0% {stroke-dashoffset: 35;}
51% {stroke-dashoffset: 35;}
52% {stroke-dashoffset: 0;}
73.999% {stroke-dashoffset: 0;}
74% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
@keyframes attempt-3-2 {
0% {stroke-dashoffset: 35;}
52% {stroke-dashoffset: 35;}
53% {stroke-dashoffset: 0;}
73.999% {stroke-dashoffset: 0;}
74% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
@keyframes attempt-3-3 {
0% {stroke-dashoffset: 35;}
53% {stroke-dashoffset: 35;}
54% {stroke-dashoffset: 0;}
73.999% {stroke-dashoffset: 0;}
74% {stroke-dashoffset: 35;}
100% {stroke-dashoffset: 35;}
}
/* Attempt n.4 */
@keyframes attempt-4-1 {
0% {stroke-dashoffset: 80;}
76% {stroke-dashoffset: 80;}
77% {stroke-dashoffset: 0;}
98.999% {stroke-dashoffset: 0;}
99% {stroke-dashoffset: 80;}
100% {stroke-dashoffset: 80;}
}
@keyframes attempt-4-2 {
0% {stroke-dashoffset: 80;}
77% {stroke-dashoffset: 80;}
78% {stroke-dashoffset: 0;}
98.999% {stroke-dashoffset: 0;}
99% {stroke-dashoffset: 80;}
100% {stroke-dashoffset: 80;}
}
@keyframes attempt-4-3 {
0% {stroke-dashoffset: 80;}
78% {stroke-dashoffset: 80;}
79% {stroke-dashoffset: 0;}
98.999% {stroke-dashoffset: 0;}
99% {stroke-dashoffset: 80;}
100% {stroke-dashoffset: 80;}
}
/* Model pattern (lines) *****************************/
.model-1 { animation: model-1 7.5s infinite; }
.model-2 { animation: model-2 7.5s infinite; }
.model-3 { animation: model-3 7.5s infinite; }
@keyframes model-1 {
0% {stroke-dashoffset: 80;}
4% {stroke-dashoffset: 80;}
8% {stroke-dashoffset: 0;}
95.999% {stroke-dashoffset: 0;}
96% {stroke-dashoffset: 80;}
100% {stroke-dashoffset: 80;}
}
@keyframes model-2 {
0% {stroke-dashoffset: 80;}
8% {stroke-dashoffset: 80;}
12% {stroke-dashoffset: 0;}
95.999% {stroke-dashoffset: 0;}
96% {stroke-dashoffset: 80;}
100% {stroke-dashoffset: 80;}
}
@keyframes model-3 {
0% {stroke-dashoffset: 80;}
12% {stroke-dashoffset: 80;}
16% {stroke-dashoffset: 0;}
95.999% {stroke-dashoffset: 0;}
96% {stroke-dashoffset: 80;}
100% {stroke-dashoffset: 80;}
}
/* Big dots (aka flash) ******************************/
.bigdot { fill-opacity: 0; }
.bigdot-1 { animation: bigdot-1 30s infinite; }
.bigdot-2 { animation: bigdot-2 30s infinite; }
.bigdot-3 { animation: bigdot-3 30s infinite; }
.bigdot-4 { animation: bigdot-4 30s infinite; }
.bigdot-5 { }
.bigdot-6 { animation: bigdot-6 30s infinite; }
.bigdot-7 { animation: bigdot-7 30s infinite; }
.bigdot-8 { animation: bigdot-8 30s infinite; }
.bigdot-9 { animation: bigdot-9 30s infinite; }
@keyframes bigdot-1 {
0.999% {fill-opacity: 0;}
1% {fill-opacity: 1;}
2% {fill-opacity: 0;}
25.999% {fill-opacity: 0;}
26% {fill-opacity: 1;}
27% {fill-opacity: 0;}
75.999% {fill-opacity: 0;}
76% {fill-opacity: 1;}
77% {fill-opacity: 0;}
}
@keyframes bigdot-2 {
1.999% {fill-opacity: 0;}
2% {fill-opacity: 1;}
3% {fill-opacity: 0;}
78.999% {fill-opacity: 0;}
79% {fill-opacity: 1;}
80% {fill-opacity: 0;}
}
@keyframes bigdot-3 {
2.999% {fill-opacity: 0;}
3% {fill-opacity: 1;}
4% {fill-opacity: 0;}
}
@keyframes bigdot-4 {
26.999% {fill-opacity: 0;}
27% {fill-opacity: 1;}
28% {fill-opacity: 0;}
}
@keyframes bigdot-6 {
3.999% {fill-opacity: 0;}
4% {fill-opacity: 1;}
5% {fill-opacity: 0;}
53.999% {fill-opacity: 0;}
54% {fill-opacity: 1;}
55% {fill-opacity: 0;}
76.999% {fill-opacity: 0;}
77% {fill-opacity: 1;}
78% {fill-opacity: 0;}
}
@keyframes bigdot-7 {
27.999% {fill-opacity: 0;}
28% {fill-opacity: 1;}
29% {fill-opacity: 0;}
50.999% {fill-opacity: 0;}
51% {fill-opacity: 1;}
52% {fill-opacity: 0;}
77.999% {fill-opacity: 0;}
78% {fill-opacity: 1;}
79% {fill-opacity: 0;}
}
@keyframes bigdot-8 {
28.999% {fill-opacity: 0;}
29% {fill-opacity: 1;}
30% {fill-opacity: 0;}
51.999% {fill-opacity: 0;}
52% {fill-opacity: 1;}
53% {fill-opacity: 0;}
}
@keyframes bigdot-9 {
52.999% {fill-opacity: 0;}
53% {fill-opacity: 1;}
54% {fill-opacity: 0;}
}
/* Result dots ***************************************/
.result-dot {
stroke-opacity: 0;
stroke: #000;
fill: #000;
}
.result-dot-1 { animation: result-dot-1 30s infinite; }
.result-dot-2 { animation: result-dot-2 30s infinite .1s; }
.result-dot-3 { animation: result-dot-3 30s infinite .2s; }
.result-dot-4 { animation: result-dot-4 30s infinite .3s; }
@keyframes result-dot-1 {
4% {stroke-opacity: 0; stroke: #000; fill: #000;}
5% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
23% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
24% {stroke-opacity: 0; stroke: #000; fill: #000;}
29% {stroke-opacity: 0; stroke: #000; fill: #000;}
30% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
48% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
49% {stroke-opacity: 0; stroke: #000; fill: #000;}
54% {stroke-opacity: 0; stroke: #000; fill: #000;}
55% {stroke-opacity: 1; stroke: #fff; fill: #000;}
73% {stroke-opacity: 1; stroke: #fff; fill: #000;}
74% {stroke-opacity: 0; stroke: #000; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill: #000;}
}
@keyframes result-dot-2 {
4% {stroke-opacity: 0; stroke: #000; fill: #000;}
5% {stroke-opacity: 1; stroke: #fff; fill: #000;}
23% {stroke-opacity: 1; stroke: #fff; fill: #000;}
24% {stroke-opacity: 0; stroke: #000; fill: #000;}
29% {stroke-opacity: 0; stroke: #000; fill: #000;}
30% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
48% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
49% {stroke-opacity: 0; stroke: #000; fill: #000;}
54% {stroke-opacity: 0; stroke: #000; fill: #000;}
55% {stroke-opacity: 1; stroke: #fff; fill: #000;}
73% {stroke-opacity: 1; stroke: #fff; fill: #000;}
74% {stroke-opacity: 0; stroke: #000; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill: #000;}
}
@keyframes result-dot-3 {
/* No step 2 and 3 */
4% {stroke-opacity: 0; stroke: #000; fill: #000;}
5% {stroke-opacity: 1; stroke: #fff; fill: #000;}
23% {stroke-opacity: 1; stroke: #fff; fill: #000;}
24% {stroke-opacity: 0; stroke: #000; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill: #000;}
}
@keyframes result-dot-4 {
/* No step 1, 2 and 3 */
79% {stroke-opacity: 0; stroke: #000; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill: #000;}
}
/* Result pos ****************************************/
.result-pos {
stroke-opacity: 0;
fill-opacity: 0;
}
.result-pos-1 { animation: result-pos-1 30s infinite; }
.result-pos-2 { animation: result-pos-2 30s infinite .1s; }
.result-pos-3 { animation: result-pos-3 30s infinite .2s; }
.result-pos-4 { animation: result-pos-4 30s infinite .3s; }
@keyframes result-pos-1 {
4% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
5% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
23% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
24% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
29% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
30% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
48% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
49% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
}
@keyframes result-pos-2 {
4% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
5% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
23% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
24% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
54% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
55% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
73% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
74% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
}
@keyframes result-pos-3 {
29% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
30% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
48% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
49% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
54% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
55% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
73% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
74% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
}
@keyframes result-pos-4 {
4% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
5% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
23% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #000;}
24% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
79% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
80% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
98% {stroke-opacity: 1; stroke: #fff; fill-opacity: 1; fill: #fff;}
99% {stroke-opacity: 0; stroke: #000; fill-opacity: 0; fill: #000;}
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 184 KiB

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="120px" height="120px" viewBox="0 0 120 120" enable-background="new 0 0 120 120" xml:space="preserve">
<path fill="#14171B" d="M119,99c0,11-9,20-20,20H21c-11,0-20-9-20-20V21C1,10,10,1,21,1h78c11,0,20,9,20,20V99z"/>
<g>
<circle fill="#FFFFFF" cx="25" cy="25" r="2"/>
<circle fill="#FFFFFF" cx="25" cy="60" r="2"/>
<circle fill="#FFFFFF" cx="25" cy="95" r="2"/>
<circle fill="#FFFFFF" cx="60" cy="25" r="2"/>
<circle fill="#FFFFFF" cx="60" cy="60" r="2"/>
<circle fill="#FFFFFF" cx="60" cy="95" r="2"/>
<circle fill="#FFFFFF" cx="95" cy="25" r="2"/>
<circle fill="#FFFFFF" cx="95" cy="60" r="2"/>
<circle fill="#FFFFFF" cx="95" cy="95" r="2"/>
<g id="theLock">
<line fill="none" stroke="#999999" stroke-width="14" stroke-linecap="round" x1="25" y1="25" x2="95" y2="60"/>
<line fill="none" stroke="#CCCCCC" stroke-width="14" stroke-linecap="round" x1="95" y1="60" x2="25" y2="95"/>
<line fill="none" stroke="#FFFFFF" stroke-width="14" stroke-linecap="round" x1="25" y1="95" x2="60" y2="25"/>
<circle fill="#999999" cx="60" cy="25" r="3.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,118 +0,0 @@
<!doctype html>
<html>
<head>
<title>BreakLock</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="shortcut icon" href="assets/favicon.ico">
<meta name="theme-color" content="#14171b">
<link rel="manifest" href="manifest.json">
<!-- Add to home screen for Safari on iOS -->
<link rel="apple-touch-icon" href="assets/icons/ios-180x180.png">
<link rel="apple-touch-startup-image" href="assets/ios-startup/startup-640x1136.png" media="device-width: 375px">
<link rel="apple-touch-startup-image" href="assets/ios-startup/startup-1080x1920.png" media="device-width: 414px">
<link rel="apple-touch-startup-image" href="assets/ios-startup/startup-640x1136.png" media="(device-width: 320px) and (device-height: 568px)">
<link rel="apple-touch-startup-image" href="assets/ios-startup/startup-640x960.png" media="(device-width: 320px) and (device-height: 480px)">
<meta name="apple-mobile-web-app-title" content="BreakLock">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no, minimal-ui">
<meta name="msapplication-TileImage" content="assets/icons/icon-144x144.png">
<meta name="msapplication-TileColor" content="#14171b">
<link rel="stylesheet" type="text/css" href="app.css">
</head>
<body>
<!-- Intro -->
<p id="app-intro" style="font-family: monospace; color: #fff;">
$ ./breaklock start<br>
Loading...
<style type="text/css">
body {
background: #14171b;
}
</style>
</p>
<!-- Static content -->
<div style="display: none;">
<div id="instructions-template">
<object class="introduction-demo" data="assets/intro.svg" type="image/svg+xml"></object>
<p>Link the dots to find the lock pattern. After every attempt the game will tell you how many dots you got right.<p>
<table>
<tr>
<td style="width:1.5em;">&#9679;</td>
<td>a dot occurs in the pattern and is in the correct order</td>
</tr>
<tr>
<td>&#9675;</td>
<td>a dot occurs in the pattern but in the wrong order</td>
</tr>
</table>
<p>The difficulty setting changes the amount of dots to connect. Easy is 4 dots, medium is 5 dots and hard is 6 dots.</p>
<p>Good luck!_</p>
<p class="small">by <a href="https://twitter.com/mxwllt"><svg class="icon"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-maxwellito"></use></svg></a> / on <a href="https://github.com/maxwellito/breaklock"><svg class="icon"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#icon-github"></use></svg></a></p>
</div>
</div>
<!-- SVG Icon definitions -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol viewBox="0 0 128 128" id="icon-new_game">
<path d="M111.5,120.5h-96c-4.418,0-8-3.582-8-8V16c0-3.236,1.949-6.153,4.938-7.391c2.991-1.237,6.432-0.554,8.718,1.734l48.5,48.5 c2.288,2.288,2.972,5.729,1.733,8.719S67.235,72.5,64,72.5H23.5v32h80v-80h-40c-4.418,0-8-3.582-8-8s3.582-8,8-8h48 c4.418,0,8,3.582,8,8v96C119.5,116.918,115.918,120.5,111.5,120.5z M23.5,56.5h21.187L23.5,35.313V56.5z"/>
</symbol>
<symbol viewBox="0 0 128 128" id="icon-back_home">
<path d="M111.5,120.5h-96c-4.418,0-8-3.582-8-8V64c0-2.129,0.849-4.17,2.358-5.672l48.25-48C59.613,8.831,61.681,8.015,63.776,8 c2.123,0.007,4.157,0.858,5.652,2.365l47.875,48.25c1.907,1.922,2.639,4.569,2.196,7.047V112.5 C119.5,116.918,115.918,120.5,111.5,120.5z M23.5,104.5h80v-32h-40c-4.418,0-8-3.582-8-8s3.582-8,8-8h29.165L63.713,27.321 L23.5,67.326V104.5z"/>
</symbol>
<symbol viewBox="0 0 128 128" id="icon-continue">
<path d="M15.5,120c-1.463,0-2.921-0.4-4.207-1.194C8.936,117.348,7.5,114.772,7.5,112V16c0-2.772,1.436-5.348,3.794-6.805 c2.359-1.458,5.304-1.591,7.784-0.35l96,48C117.788,58.2,119.5,60.97,119.5,64s-1.712,5.8-4.422,7.155l-96,48 C17.949,119.72,16.723,120,15.5,120z M23.5,28.944v70.111L93.611,64L23.5,28.944z"/>
</symbol>
<symbol viewBox="0 0 120 122" id="icon-maxwellito">
<polyline points="30,0 60,17.33 40,28.88 60,40.43 80,28.88 60,17.33 90,0 120,17.33 120,51.98 90,69.29 90,46.20 70,57.75 70,80.84 90,69.29 90,103.93 60,121.26 30,103.93 30,69.29 50,80.84 50,57.75 30,46.2 30,69.29 0,51.98 0,17.33" />
</symbol>
<symbol viewBox="0 0 128 128" id="icon-github">
<path d="M115.9,35.4c-5.3-9.2-12.6-16.5-21.8-21.8c-9.2-5.4-19.2-8-30.1-8c-10.9,0-20.9,2.7-30.1,8C24.7,18.9,17.4,26.2,12,35.4 c-5.3,9.2-8,19.3-8,30.1c0,13.1,3.8,24.9,11.4,35.3c7.7,10.4,17.5,17.7,29.6,21.7c1.4,0.3,2.5,0.1,3.1-0.5c0.7-0.6,1-1.4,1-2.4 v-4.2l-0.1-7l-1.8,0.3c-1.1,0.3-2.6,0.3-4.4,0.3c-1.7,0-3.6-0.2-5.4-0.5c-1.9-0.3-3.6-1.1-5.3-2.3c-1.6-1.2-2.7-2.8-3.4-4.8 L28,99.5c-0.5-1.2-1.3-2.5-2.5-4c-1.1-1.5-2.2-2.5-3.4-3l-0.5-0.4c-0.4-0.3-0.7-0.5-1-0.9c-0.3-0.4-0.5-0.7-0.7-1.1 c-0.1-0.4,0-0.7,0.4-0.9c0.4-0.3,1.2-0.4,2.2-0.4l1.6,0.3c1,0.2,2.3,0.8,3.8,1.8c1.6,1,2.8,2.4,3.8,4.1c1.2,2.1,2.6,3.7,4.3,4.8 c1.7,1.1,3.4,1.6,5.1,1.6c1.7,0,3.2-0.1,4.5-0.4c1.2-0.2,2.4-0.6,3.5-1.1c0.5-3.5,1.8-6.2,3.8-8c-3-0.3-5.6-0.8-8-1.4 c-2.4-0.7-4.8-1.6-7.3-3.1c-2.5-1.4-4.7-3.1-6.3-5.2c-1.6-2.1-3-4.8-4.1-8.2c-1.1-3.4-1.6-7.3-1.6-11.7c0-6.3,2.1-11.7,6.2-16.1 c-1.9-4.7-1.8-10,0.5-16c1.5-0.4,3.7-0.1,6.7,1.1c3,1.2,5.1,2.2,6.5,3c1.4,0.8,2.5,1.5,3.3,2.1c4.8-1.4,9.9-2,15-2 c5.1,0,10.1,0.7,15,2l3-1.9c2-1.3,4.4-2.5,7.2-3.5c2.7-1,4.9-1.4,6.3-0.8c2.4,5.9,2.6,11.2,0.7,15.9c4.1,4.5,6.2,9.9,6.2,16.1 c0,4.4-0.5,8.3-1.6,11.8c-1,3.4-2.4,6.1-4.1,8.2c-1.7,2-3.8,3.8-6.3,5.1c-2.6,1.5-5,2.5-7.4,3.1c-2.4,0.6-5,1.1-8,1.4 c2.7,2.4,4.1,6.1,4.1,11.1v16.5c0,0.9,0.3,1.7,1,2.4c0.7,0.6,1.6,0.8,3.1,0.5c12-4,21.9-11.2,29.6-21.7 c7.7-10.4,11.4-22.2,11.4-35.3C124,54.7,121.3,44.6,115.9,35.4L115.9,35.4z"/>
</symbol>
<symbol viewBox="0 0 128 128" id="icon-facebook">
<path d="M16,16v96h51.164V74.451h-12.08v-13.99h12.08V50.144c0-11.973,7.312-18.493,17.994-18.493c5.115,0,9.514,0.381,10.795,0.552 v12.514l-7.408,0.003c-5.809,0-6.934,2.76-6.934,6.811v8.932h13.854l-1.803,13.99H81.611V112H112V16H16z"/>
</symbol>
<symbol viewBox="0 0 128 128" id="icon-twitter">
<path d="M128,24.3c-4.7,2.1-9.8,3.5-15.1,4.1c5.4-3.2,9.6-8.4,11.5-14.5c-5.1,3-10.7,5.2-16.7,6.4 C103,15.2,96.2,12,88.6,12c-14.5,0-26.3,11.8-26.3,26.3c0,2.1,0.2,4.1,0.7,6C41.2,43.2,21.9,32.7,8.9,16.8 C6.7,20.7,5.4,25.2,5.4,30c0,9.1,4.6,17.1,11.7,21.9c-4.3-0.1-8.4-1.3-11.9-3.3v0.3c0,12.7,9.1,23.3,21.1,25.7 c-2.2,0.6-4.5,0.9-6.9,0.9c-1.7,0-3.3-0.2-4.9-0.5c3.3,10.4,13,18,24.5,18.2c-9,7-20.3,11.2-32.6,11.2c-2.1,0-4.2-0.1-6.3-0.4 c11.6,7.5,25.4,11.8,40.2,11.8c48.3,0,74.7-40,74.7-74.7l-0.1-3.4C120,34.2,124.5,29.6,128,24.3z"/>
</symbol>
</svg>
<noscript>
<!--[if lte IE 6]>
<p>
If you read this it's probably because the world has rebooted. I don't know how but you arrived here. I'm sorry you cannot enjoy this little game but you will be able to in the not too distant future. Browsers will become incredibly better. Just before the world turns into chaos due to Nutella production shortages. Scientists will realise too late that Nutella was to humans what pollen is to bees. Don't believe the hype, the end didn't (or won't) happen because the 'left-pad' package got removed from NPM. Good luck.
</p>
<p>
PS: If the world has ended, please tell thejameskyle that I loved him. Only his cat can save the world. (Shut the door)
</p>
<![endif]-->
</noscript>
<script src="app.js"></script>
<script type="text/javascript">
// Set up service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service-worker.js', {scope: '.'});
}
// Easter Egg to get it full black
// Awesome for OLED screens
if (localStorage.getItem('isDeepBlack')) {
document.body.classList.add('deepblack')
}
</script>
</body>
</html>

View File

@ -1,55 +0,0 @@
#!/usr/bin/env node
if (!process.argv[2] || process.argv[2])
var patternLength = parseInt(process.argv[2]),
limit = 9,
proxies = [
[0,2,1],
[2,0,1],
[2,8,5],
[8,2,5],
[6,8,7],
[8,6,7],
[0,6,3],
[6,0,3],
[0,8,4],
[8,0,4],
[2,6,4],
[6,2,4]
]
if (!patternLength || patternLength < 1 || patternLength > limit) {
console.log('Usage: node bruteCalc.js patternLength')
console.log('Please use a valid \'patternLength\' value (between 1 and 9)')
process.exit(1)
}
function bf (length, stack, buffer) {
buffer = !buffer ? [] : buffer;
if (length <= 0) {
stack.push(buffer)
return stack
}
for (var i = 0; i < limit; i++) {
if (buffer.indexOf(i) != -1) {
continue
}
let pop = buffer[(buffer.length || 1) - 1]
if (buffer.length > 0 && proxies.find(pr => pr[0] == pop && pr[1] == i && buffer.indexOf(pr[2]) == -1)) {
continue
}
let clone = buffer.concat([])
clone.push(i)
bf(length-1,stack,clone)
}
return stack
}
var bfList = bf(patternLength, [])
bfList.forEach(s => console.log(s.join('')))
console.log('-----')
console.log('Pattern length : ' + patternLength)
console.log('Lock founds : ' + bfList.length)

View File

@ -1,244 +0,0 @@
/**
* Dirty code to inject into a page to generate
* the cover for the App in SVG
*/
/**
* Pattern class
* a pattern is a object representation of a combinaison.
* The amount of dot is specified in the constructor, it's
* not linked to the class itself.
*
* For reference:
* 0 1 2
* 3 4 5
* 6 7 8
*
*/
class Pattern {
constructor (dotLength) {
this.dotLength = dotLength
this.suite = []
}
/**
* Fill the current instance with random values
*/
fillRandomly () {
while (!this.isComplete()) {
this.addDot(Math.floor(Math.random() * 9))
}
}
/**
* Add point to the current pattern
* @param {int} dotIndex Dot index to add
* @return boolean True if successfully added
*/
addDot (dotIndex) {
// Test if the dot can be added
if (this.isComplete() || ~this.suite.indexOf(dotIndex))
return [];
// Test for potential median dot
let lastDot = this.suite[this.suite.length - 1],
medianDot = (lastDot + dotIndex) / 2
if (lastDot != undefined &&
medianDot >> 0 === medianDot &&
(lastDot%3) - (medianDot%3) === (medianDot%3) - (dotIndex%3) &&
Math.floor(lastDot/3) - Math.floor(medianDot/3) === Math.floor(medianDot/3) - Math.floor(dotIndex/3)) {
let addedPoints = this.addDot(medianDot)
if (!this.isComplete()) {
this.suite.push(dotIndex)
addedPoints.push(dotIndex)
}
return addedPoints
}
this.suite.push(dotIndex)
return [dotIndex]
}
/**
* Checks if the instance suite is complete
* @return {Boolean}
*/
isComplete () {
return this.suite.length >= this.dotLength
}
/**
* Checks if a dot is already in the pattern
* @param {int} dotIndex Index to check
* @return {boolean}
*/
gotDot (dotIndex) {
return ~this.suite.indexOf(dotIndex)
}
compare (pattern) {
var goodPos = 0,
wrongPos = 0
for (let i = 0; i < this.dotLength; i++) {
if (this.suite[i] === pattern.suite[i])
goodPos++
for (let j = 0; j < this.dotLength; j++) {
if (this.suite[j] === pattern.suite[i])
wrongPos++
}
}
return [goodPos, wrongPos - goodPos, this.dotLength - wrongPos]
}
/**
* Reset the pattern by removing all the dots
*/
reset () {
this.suite = []
}
}
class PatternSVG {
constructor () {
this.el = document.createElementNS(this.SVG_NAMESPACE, 'svg')
this.el.setAttribute('viewBox', '0 0 ' + (this.GRID_GUTTER * 3 * width) + ' ' + (this.GRID_GUTTER * 3 * height))
}
/**
* Add pattern to the instance
* @param {Pattern} pattern Pattern instance to get the points from
* @param {int} size Thickness of the line
* @param {string|array} color List of colors to use for the pattern
* @return PatternSVG
*/
addPattern (pattern, size = 14, color = '#fff', x, y) {
let lineGroup = document.createElementNS(this.SVG_NAMESPACE, 'g')
color = color instanceof Array ? color : [color]
lineGroup.setAttribute('stroke-width', size)
lineGroup.setAttribute('stroke-linecap', 'round')
this.el.appendChild(lineGroup)
for (let i = 1; i < pattern.suite.length; i++) {
let line = document.createElementNS(this.SVG_NAMESPACE, 'line')
line.setAttribute('x1', (x * 3 * this.GRID_GUTTER) + (pattern.suite[i-1] % 3) * this.GRID_GUTTER)
line.setAttribute('y1', (y * 3 * this.GRID_GUTTER) + Math.floor(pattern.suite[i-1] / 3) * this.GRID_GUTTER)
line.setAttribute('x2', (x * 3 * this.GRID_GUTTER) + (pattern.suite[i] % 3) * this.GRID_GUTTER)
line.setAttribute('y2', (y * 3 * this.GRID_GUTTER) + Math.floor(pattern.suite[i] / 3) * this.GRID_GUTTER)
line.setAttribute('stroke', color[Math.min(color.length, i) - 1])
lineGroup.appendChild(line)
}
// Add the dot reprenting the final dot
let lastDotIndex = pattern.suite[pattern.suite.length - 1]
let lastDot = document.createElementNS(this.SVG_NAMESPACE, 'circle')
lastDot.setAttribute('cx', (x * 3 * this.GRID_GUTTER) + (lastDotIndex % 3) * this.GRID_GUTTER)
lastDot.setAttribute('cy', (y * 3 * this.GRID_GUTTER) + Math.floor(lastDotIndex / 3) * this.GRID_GUTTER)
lastDot.setAttribute('fill', color[0])
lastDot.setAttribute('r', size / 4)
lineGroup.appendChild(lastDot)
return lineGroup
}
/**
* Add dots to the instance
* @param {int} size Thickness of the line
* @param {object} attr List of attributes to set to the group
* @return PatternSVG
*/
addDots (size = 3, attr = {}) {
attr.fill = attr.fill || '#fff'
let dotGroup = this.addGroup(attr)
for (let x = 0; x < 3 * width; x++) {
for (let y = 0; y < 3 * height; y++) {
let circle = document.createElementNS(this.SVG_NAMESPACE, 'circle')
circle.setAttribute('cx', x * this.GRID_GUTTER)
circle.setAttribute('cy', y * this.GRID_GUTTER)
circle.setAttribute('r', size)
dotGroup.appendChild(circle)
}
}
return dotGroup
}
/**
* Add the darkbackground
* @return PatternSVG
*/
addBackground () {
let rect = document.createElementNS(this.SVG_NAMESPACE, 'rect')
rect.setAttribute('width', width * 3 * this.GRID_GUTTER)
rect.setAttribute('height', height * 3 * this.GRID_GUTTER)
rect.setAttribute('fill', '#14171b')
this.el.appendChild(rect)
return rect
}
/**
* Add group to the instance
* @param {object} attr List of attributes to set to the group
* @return PatternSVG
*/
addGroup (attr) {
let group = document.createElementNS(this.SVG_NAMESPACE, 'g')
for (var key in attr) {
group.setAttribute(key, attr[key])
}
this.el.appendChild(group)
return group
}
/**
* Get the SVG
* @return SVGDOMElement
*/
getSVG () {
return this.el
}
}
PatternSVG.prototype.SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
PatternSVG.prototype.SVG_WIDTH = 100
PatternSVG.prototype.SVG_COMB_EXP = 20
PatternSVG.prototype.SVG_MARGIN = 15
PatternSVG.prototype.GRID_GUTTER = 35
PatternSVG.prototype.DOT_BORDER = 2
PatternSVG.prototype.DOT_MAGNET = 6
var width = 12
var height = 18
var mySVG = new PatternSVG()
mySVG.addBackground()
mySVG.addDots(2)
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
let pat = new Pattern(4)
pat.fillRandomly()
mySVG.addPattern(pat, 14, ['#999','#ccc','#fff'], x, y) //# TO_DO: Need consts
}
}
document.body.appendChild(mySVG.el)

View File

@ -1,30 +0,0 @@
{
"name": "BreakLock",
"short_name": "BreakLock",
"icons": [{
"src": "./assets/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "./assets/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "./assets/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "./assets/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "./assets/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}],
"start_url": "./",
"display": "standalone",
"orientation": "portrait",
"background_color": "#14171b",
"theme_color": "#14171b"
}

View File

@ -1,31 +0,0 @@
{
"name": "breaklock",
"version": "0.1.0",
"description": "Small PWA game to guess pattern locks",
"main": "scripts/app.js",
"scripts": {
"build": "./node_modules/webpack/bin/webpack.js --optimize-minimize"
},
"repository": {
"type": "git",
"url": "https://github.com/maxwellito/breaklock.git"
},
"author": "maxwellito",
"license": "MIT",
"bugs": {
"url": "https://github.com/maxwellito/breaklock/issues"
},
"homepage": "https://github.com/maxwellito/breaklock",
"devDependencies": {
"babel-core": "^6.24.0",
"babel-loader": "^6.4.1",
"babel-preset-es2015": "^6.24.0",
"css-loader": "^0.28.0",
"extract-text-webpack-plugin": "^2.1.0",
"node-sass": "^4.5.2",
"raw-loader": "^0.5.1",
"sass-loader": "^6.0.3",
"style-loader": "^0.16.1",
"webpack": "^2.3.3"
}
}

View File

@ -1,30 +0,0 @@
![BreakLock](assets/banner.png)
### Bring me to the **[game](https://maxwellito.github.io/breaklock/)**!
Silly HTML5 game, mobile first.
BreakLock is a hybrid of Mastermind and the Android pattern lock. A game you gonna love to hate.
Hopefully this game (codebase included) will drive you mad.
The goal of this project was the discover progressive Web apps with service workers, and play with Webpack. Also to entertain because the tube is quite boring, especially the Central line on peak time.
If you like this game, you must convince 3 people to like it, who will have to convince 3 other people... and this project will turn into the first OSS sect. Also, if you hate it, you must convince 3 people to hate it, who will have to convince 3 other people...
As long as GitHub provide these sweet gh-pages, this project will be under MIT, without ads, free.
If you're curious about pattern combinations, run `./lab/bruteCalc.js`.
## Contribute
Contributions are welcome, especially pull-requests. There are a lot of ideas to implement, but less people when it come to code. This is why **issues are for bugs only**.
## Build
```bash
# Install packages
npm install
# Build it
npm run build
```

View File

@ -1,79 +0,0 @@
var APP_NAME = 'breaklock',
APP_VERSION = 12,
CACHE_NAME = APP_NAME + '_' + APP_VERSION;
var filesToCache = [
'./',
'./?utm_source=homescreen',
'./app.css',
'./app.js',
'./assets/intro.svg',
'./assets/fonts/robotomono-light-webfont.woff2',
'./assets/fonts/robotomono-light-webfont.woff',
'./assets/fonts/robotomono-light-webfont.ttf'
];
// Service worker from Google Documentation
self.addEventListener('install', function(event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME)
.then(function(cache) {
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName.indexOf(APP_NAME) === 0 && CACHE_NAME !== cacheName) {
return caches.delete(cacheName);
}
})
);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
// IMPORTANT: Clone the request. A request is a stream and
// can only be consumed once. Since we are consuming this
// once by cache and once by the browser for fetch, we need
// to clone the response.
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then(
function(response) {
// Check if we received a valid response
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});

View File

@ -1,40 +0,0 @@
$theme_dark: #14171b;
$theme_bright: #ffffff;
$theme_success: #116699;
$theme_error: #ff0000;
$gutter: 2rem;
$border: 1px dotted $theme_bright;
$font_weight: 300;
// Media queries sizes
$mq-small-min: 0px;
$mq-medium-min: 480px;
$mq-large-min: 768px;
$mq-xlarge-min: 1024px;
$mq-xxlarge-min: 1600px;
$mq-small-max: $mq-medium-min - 1px;
$mq-medium-max: $mq-large-min - 1px;
$mq-large-max: $mq-xlarge-min - 1px;
$mq-xlarge-max: $mq-xxlarge-min - 1px;
$mq-xxlarge-max: 5120px;
// Mixins
%button-feel {
@extend %unselectable;
cursor: pointer;
}
%unselectable {
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
/* Rules below not implemented in browsers yet */
-o-user-select: none;
user-select: none;
}

View File

@ -1,31 +0,0 @@
import GameCtrl from './controllers/game/game.ctrl'
import MenuCtrl from './controllers/menu/menu.ctrl'
require('./style.scss');
var introDom = document.getElementById('app-intro')
if (introDom) {
introDom.remove()
}
var container = document.body
var myGame = new GameCtrl(() => {
myMenu.el.style.display = ''
myGame.el.style.display = 'none'
})
container.appendChild(myGame.el)
var myMenu = new MenuCtrl((type, difficulty) => {
myGame.start(type, difficulty)
myMenu.el.style.display = 'none'
myGame.el.style.display = ''
})
myMenu.init()
container.appendChild(myMenu.el)
myGame.el.style.display = 'none'
window.scrollTo(0, 0)

View File

@ -1,54 +0,0 @@
const config = {
GAME: {
DIFFICULTY: {
EASY: 4,
MEDIUM: 5,
HARD: 6
},
TYPE: {
PRACTICE: 1,
CHALLENGE: 2,
COUNTDOWN: 3
},
ACTIONS: {
CONTINUE: 1,
NEW_GAME: 2,
BACK_HOME: 3
}
},
SOCIAL: {
PLATFORMS: {
FB: {
NAME: 'Facebook',
ICON: 'facebook',
URL: (url) => `https://www.facebook.com/sharer/sharer.php?u=${encodeURI(url)}`
},
TWITTER: {
NAME: 'Twitter',
ICON: 'twitter',
URL: (url, msg, tags) => {
return `http://twitter.com/` +
(url ? `share?` : `intent/tweet?`) +
(msg ? `text=${encodeURI(msg)}&` : '') +
(url ? `url=${encodeURI(url)}&` : '') +
(tags ? `hashtags=${encodeURI(tags.join(','))}` : '')
}
}
},
MESSAGE: 'I wasted my time on BreakLock, it\'s pointless, don\'t try it.',
TAGS: ['breaklock']
},
URL: 'https://maxwellito.github.io/breaklock/',
COLORS: {
BRIGHT: '#ffffff',
DARK: '#14171b',
SUCCESS: '#116699',
ERROR: '#ff0000'
},
PATTERN: {
HEX_COLOR_START: '66',
HEX_COLOR_END: 'FF'
}
}
export default config

View File

@ -1,98 +0,0 @@
import leftPadNum from '../../utils/leftPadNum'
import dom from '../../utils/dom'
require('./countdown.scss');
/**
* Countdown Controller
* Basic component representing a small countdown
* bar to be used in the status bar.
*/
class CountdownCtrl {
/**
* Not much here... just buildin the template
*/
constructor () {
this.setupTemplate()
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
this.counterEl = dom.create('span', 'countdown-counter')
this.barEl = dom.create('span', 'countdown-content')
let container = dom.create('span', 'countdown-container', [
this.barEl
])
this.el = dom.create('div', 'countdown', [
this.counterEl,
container
])
return this.el
}
/**
* Set the countdown.
* @param {int} duration Duration in seconds
* @param {function} callback Callback to call on end
*/
setTimer (duration, callback) {
this.duration = duration
this.remaining = duration
this.endCallback = callback
this.render()
}
/**
* Starts the countdown
*
*/
start () {
if (this.interval)
return
this.interval = window.setInterval(this.decrement.bind(this), 1000)
}
/**
* Stops the countdown
*
*/
stop () {
window.clearInterval(this.interval)
this.interval = null
}
/**
* Decrement the counter by one second
*
*/
decrement () {
this.remaining--
this.render()
}
/**
* Render the component by using the values of
* the instance. If the countdown is negative or
* null, the end callback will be triggered
*/
render () {
this.remaining = this.remaining > 0 ? this.remaining : 0
this.el.classList[this.remaining > 10 ? 'remove' : 'add']('alert')
this.counterEl.textContent = leftPadNum(this.remaining, 3)
this.barEl.style.width = (this.remaining / this.duration * 100) + '%'
if (this.remaining == 0) {
this.stop()
this.endCallback && this.endCallback()
}
}
}
export default CountdownCtrl

View File

@ -1,26 +0,0 @@
@import "../../_variables.scss";
.countdown-container {
float: right;
width: 10rem;
height: 1rem;
margin: .25rem 0 .25rem .5rem;
border: 1px solid currentColor;
border-radius: .25rem;
box-sizing: border-box;
color: inherit;
}
.alert {
color: red;
animation: blink 1s infinite;
-webkit-animation: blink 1s infinite;
}
.countdown-content {
display: block;
height: 100%;
width: 0;
color: inherit;
background-color: currentColor;
}

View File

@ -1,72 +0,0 @@
import dom from '../../utils/dom'
require('./extender.scss');
/**
* Extender class
* Simple controller to show/hide long content
* with a button.
*/
class ExtenderCtrl {
/**
* Every instance require a title which is the dropdown
* button text, the content that can be a string or
* a DOM element to display, and the initial state
* of the extender.
* @param {String} title Button title
* @param {String} content Content text of DOM element to display
* @param {Boolean} isExpanded Initial state of the controller
*/
constructor (title, content, isExpanded) {
this.title = title
this.content = content
this.isExpanded = isExpanded
this.setupTemplate()
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
let content = this.content instanceof String ? this.content : [this.content]
this.buttonEl = dom.create('button', 'extender-button', this.title)
this.contentEl = dom.create('div', 'extender-content', content)
this.el = dom.create('div', 'extender small-only', [
this.buttonEl,
this.contentEl
])
this.render()
return this.el
}
/**
* Set up listeners
*/
init () {
this.buttonEl.addEventListener('click', this.toggle.bind(this))
}
/**
* Show/hide the content
* @param {Boolean} force Force to show or hide if provided
*/
toggle (force) {
this.isExpanded = (force instanceof Boolean) ? force : !this.isExpanded
this.render()
}
/**
* Render the DOM from the state of the controller
*
*/
render () {
this.el.classList[this.isExpanded ? 'add' : 'remove']('active')
}
}
export default ExtenderCtrl

View File

@ -1,45 +0,0 @@
@import "../../_variables.scss";
// Not much for now...
.extender {
}
.extender-button {
display: block;
width: 100%;
padding: .25em;
text-align: center;
background: rgba(255,255,255,.125);
border-bottom: $border;
&:after {
//
content: '';
margin-left: .5em;
}
.extender.active &:after {
//
content: '';
}
}
.extender-content {
display: none;
.extender.active & {
display: inherit;
}
}
@media (orientation: landscape), (min-width: $mq-xlarge-min) {
.extender.small-only {
.extender-button {
display: none;
}
.extender-content {
display: inherit;
}
}
}

View File

@ -1,210 +0,0 @@
import StatusBarCtrl from '../statusBar/statusBar.ctrl'
import HistoryCtrl from '../history/history.ctrl'
import LockCtrl from '../lock/lock.ctrl'
import SummaryCtrl from '../summary/summary.ctrl'
import Pattern from '../../models/pattern'
import PatternSVG from '../../utils/patternSVG'
import config from '../../config'
import dom from '../../utils/dom'
import color from '../../utils/color'
require('./game.scss');
/**
* Game Controller
* The playground, the arena
* It combines a status bar, a pattern history
* and a lock.
*/
class GameCtrl {
/**
* Setup the controller
* The callback provided will be called with
* different parameter depending on the type
* of end (abort, success, fail)
* @param {function} onEnd Callback for end of game
* @return {[type]} [description]
*/
constructor (onEnd) {
// Lets leave it empty for now
// just init the shite to help V8
this.statusBar = new StatusBarCtrl(this.abort.bind(this))
this.history = new HistoryCtrl()
this.lock = new LockCtrl(this.newAttempt.bind(this))
this.summary = new SummaryCtrl(this.action.bind(this))
this.pattern = null
this.type = null
this.isEnded = false
this.onEnd = onEnd
this.statusBar.init()
this.lock.init()
this.setupTemplate()
}
/**
* Build template of the controller
* @return {SVGDOMElement}
*/
setupTemplate () {
this.el = dom.create('div', 'game-layout view', [
dom.create('div', 'view-bloc game-layout-dashboard', [
this.statusBar.el,
dom.create('div', 'history-wrap', [this.history.el])
]),
dom.create('div', 'view-bloc game-layout-lock', [this.lock.el]),
this.summary.el
])
return this.el
}
/* Controls **********************************/
/**
* Start a new game
* @param {int} type Type ID
* @param {int} difficulty Number of dots
*/
start (type, difficulty) {
this.type = type
this.difficulty = difficulty
this.lock.setDotLength(difficulty)
this.pattern = new Pattern(difficulty)
this.pattern.fillRandomly()
this.history.clear('Connect ' + difficulty + ' dots')
this.count = 0
this.isEnded = false
switch (type) {
case config.GAME.TYPE.PRACTICE:
return this.statusBar.setCounter(0)
case config.GAME.TYPE.CHALLENGE:
return this.statusBar.setCounter(10)
case config.GAME.TYPE.COUNTDOWN:
return this.statusBar.setCountdown(60)
}
}
/**
* Listener for new pattern drawn by the user
* and provided via the the Lock controller.
* @param {Pattern} pattern Pattern to test
* @return {Boolean} True if the pattern is correct
*/
newAttempt (pattern) {
// Generate a SVG from the pattern provided
let match = this.pattern.compare(pattern),
svgPattern = this.buildPatternSVG(pattern, match),
isUnlocked = (match[0] === this.pattern.dotLength)
this.count++
if (this.isEnded) {
this.statusBar.incrementCounter()
}
else if (isUnlocked) {
// Success case
if (this.type === config.GAME.TYPE.COUNTDOWN)
this.statusBar.stopCountdown()
this.isEnded = svgPattern
this.summary.setContent(true, this.count)
}
else {
// Fail case
switch (this.type) {
case config.GAME.TYPE.PRACTICE:
this.statusBar.incrementCounter()
break
case config.GAME.TYPE.CHALLENGE:
if (this.statusBar.decrementCounter() === 0) {
this.isEnded = true
this.summary.setContent(false, this.count)
}
break
}
}
this.history.stackPattern(svgPattern)
return isUnlocked
}
/**
* Cancel listener for the status bar
* @param {Number} exitCode Exit code from the status bar
*/
abort (exitCode) {
if (exitCode) {
// Exit from countdown
this.isEnded = true
this.statusBar.stopCountdown()
this.summary.setContent(false, this.count)
}
else {
// Abort from the user
this.onEnd()
}
}
/**
* Action listener for the action of the summary
* controller. To continue, try again or go back
* to the home menu.
* @param {Number} actionId Action code to apply
*/
action (actionId) {
switch (actionId) {
case config.GAME.ACTIONS.NEW_GAME:
this.start(this.type, this.difficulty)
break;
case config.GAME.ACTIONS.BACK_HOME:
this.abort()
break;
case config.GAME.ACTIONS.CONTINUE:
// Nothing for now
debugger
if (this.isEnded === true) {
let match = this.pattern.compare(this.pattern)
let svgPattern = this.buildPatternSVG(this.pattern, match)
this.history.stackPattern(svgPattern)
}
this.statusBar.setCounter(this.count)
break;
}
this.summary.toggle()
}
/**
* Generate the pattern SVG from a pattern object
*
* @param {Pattern} pattern Pattern object to use to generate the SVG
* @param {Array} match Pattern match array
* @return {SVGDOMElement}
*/
buildPatternSVG (pattern, match) {
// Generate a SVG from the pattern provided
let attemptSVG = new PatternSVG()
attemptSVG.addDots(1)
attemptSVG.addPattern(pattern, 14, color.greydient(
config.PATTERN.HEX_COLOR_START,
config.PATTERN.HEX_COLOR_END,
pattern.dotLength - 3
))
// Add the feedback on the SVG
if (match)
PatternSVG.prototype.addCombinaison.apply(attemptSVG, match)
let svgPattern = attemptSVG.getSVG()
// Add the success class to the SVG
if ((match[0] === pattern.dotLength))
svgPattern.classList.add('success')
return svgPattern
}
}
export default GameCtrl

View File

@ -1,20 +0,0 @@
@import "../../_variables.scss";
@media (max-width: $mq-large-max) {
.history-wrap {
border-bottom: $border;
.history {
margin: 0 #{-$gutter};
}
.history-container {
padding: 0 #{$gutter};
}
}
}
@media (orientation: landscape), (min-width: $mq-xlarge-min) {
.game-layout-lock {
align-self: center;
}
}

View File

@ -1,79 +0,0 @@
import dom from '../../utils/dom'
require('./history.scss');
/**
* History Controller
* Simple controller stacking the previous pattern
* tested by the user. This is a simple dummy container
* stacking all the SVGs
* The class used is .history, for optimisation all
* SVGs will use `will-change` or `transformZ` properties
* to improve performance.
*/
class HistoryCtrl {
/**
* Set up instance
*/
constructor () {
this.lastPattern = null
this.setupTemplate()
}
/**
* Build template of the controller
* @return {SVGDOMElement}
*/
setupTemplate () {
this.containerEl = dom.create('div', 'history-container', '')
this.el = dom.create('div', 'history', [this.containerEl])
return this.el
}
/**
* Add a new pattern in the container
* @param {SVGDOM} pattern The try to stack
*/
stackPattern (pattern) {
if (this.lastPattern)
this.containerEl.insertBefore(pattern, this.lastPattern)
else
this.containerEl.appendChild(pattern)
this.lastPattern = pattern
this.scrollToStart()
}
/**
* Loop animation to scroll smoothly
* the history to the start.
*/
scrollToStart () {
let pos = this.el.scrollLeft
this.el.scrollLeft = (pos - Math.max(pos / 4, 4), 0)
if (this.el.scrollLeft > 0) {
window.requestAnimationFrame(this.scrollToStart.bind(this))
}
}
/**
* Clean the history
*
* @param {String} helperText Helper text displayed when the history is empty
* @return {[type]}
*/
clear (helperText) {
this.lastPattern = null
// Clean the container
this.containerEl.remove()
this.containerEl = dom.create('div', {
class: 'history-container',
'data-helper': helperText
})
this.el.appendChild(this.containerEl)
}
}
export default HistoryCtrl

View File

@ -1,100 +0,0 @@
@import "../../_variables.scss";
$patternHeight: 5rem;
$patternMargin: 1rem;
$patternRatio: 100/120;
// I really wanted to use width: auto.
// But not all browsers are ok with it
.history {
height: $patternHeight + (2 * $patternMargin);
overflow-x: scroll;
font-size: 0;
line-height: 0;
white-space: nowrap;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.history-container {
display: table-cell;
vertical-align: top;
&:empty {
display: block;
&:after {
content: attr(data-helper);
display: block;
line-height: 6;
font-size: 1rem;
text-align: center;
}
}
}
.history-start-helper {
display: none;
}
.history svg {
width: $patternHeight * $patternRatio;
height: $patternHeight;
display: inline-block;
margin: $patternMargin 0;
transform: translateZ(0);
animation: expand .25s, fadein .5s linear;
-webkit-transform: translateZ(0);
-webkit-animation: expand .25s, fadein .5s linear;
&.success {
border: #{$patternMargin/8} solid currentColor;
border-radius: #{$patternMargin/4};
padding: #{$patternMargin/8*3} #{$patternMargin/4};
margin-top: #{$patternMargin/2};
margin-bottom: #{$patternMargin/2};
}
& + svg {
margin-left: $patternMargin;
}
}
@keyframes fadein {
0% {opacity: 0;}
50% {opacity: 0;}
100% {opacity: 1;}
}
@-webkit-keyframes fadein {
0% {opacity: 0;}
50% {opacity: 0;}
100% {opacity: 1;}
}
@keyframes expand {
0% {width: 0; padding: 0;}
}
@-webkit-keyframes expand {
0% {width: 0; padding: 0;}
}
@media (orientation: landscape), (min-width: $mq-xlarge-min) {
.history {
height: auto;
border-bottom: none;
}
.history-container {
display: block;
}
.history svg {
float: left;
margin-right: $patternMargin;
& + svg {
margin-left: 0;
}
}
}

View File

@ -1,311 +0,0 @@
import Pattern from '../../models/pattern'
import PatternSVG from '../../utils/patternSVG'
import dom from '../../utils/dom'
import config from '../../config'
require('./lock.scss');
/**
* Lock class
* Component of the lock itself
* It will listen for user input, update the view
* and trigger the other instances when required.
*/
class LockCtrl {
/**
* Make it ready
* @param {Function} callback Callback to call on new pattern
*/
constructor (callback) {
this.currentLine = null
this.onNewPattern = callback
this.setupTemplate()
}
/**
* Build template of the controller
* @return {SVGDOMElement}
*/
setupTemplate () {
let myPatternSVG = new PatternSVG()
myPatternSVG.addBackgroundLayer()
this.el = myPatternSVG.getSVG()
this.el.setAttribute('class', 'lock')
this.patternEl = myPatternSVG.addGroup({
'stroke-width': '2',
'stroke': config.COLORS.BRIGHT,
'stroke-linecap': 'round'
})
this.bigDotsEl = myPatternSVG.addDots(9, {class: 'lock-flashdots'})
myPatternSVG.addDots(2)
return this.el
}
/**
* Start listening to user input
*/
init () {
// start listening for events (fingers)
this.el.addEventListener('touchstart', this.touchStart.bind(this))
this.el.addEventListener('touchmove', this.touchUpdate.bind(this))
this.el.addEventListener('touchend', this.touchEnd.bind(this))
// start listening for mouse
this.el.addEventListener('mousedown', this.mouseStart.bind(this))
}
/**
* Set the pattern length
* @param {int} dotLength Number of dots in the pattern
*/
setDotLength (dotLength) {
this.dotLength = dotLength
this.pattern = new Pattern(this.dotLength)
}
/**
* Listeners
*/
/* Mouse listeners *************************************/
/**
* Listener for mouse down on the lock.
* It will start listening to mouse move and stop.
* @param {MouseEvent} t Mouse down event
*/
mouseStart (t) {
this.reset()
this.mouseUpdateBind = this.mouseUpdate.bind(this)
this.mouseEndBind = this.mouseEnd.bind(this)
this.el.addEventListener('mousemove', this.mouseUpdateBind)
window.addEventListener('mouseleave', this.mouseEndBind)
window.addEventListener('mouseup', this.mouseEndBind)
this.mouseUpdate(t)
}
/**
* Listener for mouse move while drowing a pattern
* with the mouse.
* @param {MouseEvent} t Mouse move event
*/
mouseUpdate (t) {
t.preventDefault()
t.stopPropagation()
let e = t.currentTarget.getBoundingClientRect(),
x = Math.max(0, Math.min(PatternSVG.prototype.SVG_WIDTH, Math.round(PatternSVG.prototype.SVG_WIDTH / e.width * (t.pageX - e.left)))),
y = Math.max(0, Math.min(PatternSVG.prototype.SVG_WIDTH, Math.round(PatternSVG.prototype.SVG_WIDTH / e.height * (t.pageY - e.top ))))
this.updatePoint(x, y)
}
/**
* Method to end drawing with mouse.
* It will remove useless listeners and reset
* the current pattern.
* @param {MouseEvent} t Mouse event
*/
mouseEnd (t) {
if (!this.isPendingReset)
this.reset()
this.el.removeEventListener('mousemove', this.mouseUpdateBind)
window.removeEventListener('mouseout', this.mouseEndBind)
window.removeEventListener('mouseup', this.mouseEndBind)
}
/* Touch listeners *************************************/
/**
* Listener for startring drwaing a pattern
* with touch events.
* This will only reset the current pattern
* and start drawing the new one.
* @param {Event} t Touch Start event
*/
touchStart (t) {
this.reset()
this.touchUpdate(t)
}
/**
* Listener for touch events
* The method will calculate the position of the finger
* on the lock to update the line and add dots to the
* current pattern.
* @param {Event} t Touch event
*/
touchUpdate (t) {
t.preventDefault()
t.stopPropagation()
let e = t.currentTarget.getBoundingClientRect(),
x = Math.max(0, Math.min(PatternSVG.prototype.SVG_WIDTH, Math.round(PatternSVG.prototype.SVG_WIDTH / e.width * (t.targetTouches[0].pageX - e.left)))),
y = Math.max(0, Math.min(PatternSVG.prototype.SVG_WIDTH, Math.round(PatternSVG.prototype.SVG_WIDTH / e.height * (t.targetTouches[0].pageY - e.top))))
this.updatePoint(x, y)
}
/**
* Listener for end of pattern drawing
* with touch events.
*/
touchEnd () {
if (!this.isPendingReset)
this.reset()
}
/*
* Drawing logic
*/
/**
* Update the current pattern by providing
* the position of the new cursor/finger
* in ordinates scaled to SVG size.
* @param {Number} x Position X of pointer/finger
* @param {Number} y Position X of pointer/finger
*/
updatePoint (x, y) {
// Cancel if the current pattern is finished
if (this.isPendingReset)
return
let iX, iY
for (let i = 0; i < 3; i++) {
let rangeStart = PatternSVG.prototype.GRID_GUTTER * i + PatternSVG.prototype.SVG_MARGIN - PatternSVG.prototype.DOT_MAGNET,
rangeEnd = PatternSVG.prototype.GRID_GUTTER * i + PatternSVG.prototype.SVG_MARGIN + PatternSVG.prototype.DOT_MAGNET
iX = (rangeStart <= x && rangeEnd >= x) ? i : iX
iY = (rangeStart <= y && rangeEnd >= y) ? i : iY
}
let isEndOfPattern
if (iX !== undefined && iY != undefined) {
let dotIndex = iY * 3 + iX
isEndOfPattern = this.triggerDot(dotIndex)
}
if (!isEndOfPattern)
this.updateLine(x, y)
return true
}
/**
* Add a dot on the current pattern and the
* intermediate ones if there's any.
* @param {number} dotIndex Dot triggered index
*/
triggerDot (dotIndex) {
// Check if the current pattern got the dot
if (this.pattern.gotDot(dotIndex))
return
// Get the list new dots from the one triggered.
// This method return a list because adding a
// dot might add some intermediate others.
var newDots = this.pattern.addDot(dotIndex)
if (navigator.vibrate)
navigator.vibrate(20)
newDots.forEach((dot, index) => {
let dotX = PatternSVG.prototype.GRID_GUTTER * (dot % 3) + PatternSVG.prototype.SVG_MARGIN,
dotY = PatternSVG.prototype.GRID_GUTTER * Math.floor(dot / 3) + PatternSVG.prototype.SVG_MARGIN
// Close current line
this.closeLine(dotX, dotY)
this.bigDotsEl.childNodes[dot].classList.add('active')
// Check if finished
if ((index + 1) === newDots.length && this.pattern.isComplete())
// The drawing here
return this.checkPattern()
else
// Start new one
this.startLine(dotX, dotY)
})
}
/**
* Reset the lock
*/
reset () {
// Clear timeout
clearTimeout(this.isPendingReset)
this.isPendingReset = null
// Reset pattern
this.pattern.reset()
// Clear existing pattern
this.currentLine = null
for (let i = 0; i < 9; i++)
this.bigDotsEl.childNodes[i].classList.remove('active')
for (let i = this.patternEl.childNodes.length - 1; i >= 0; i--)
this.patternEl.childNodes[i].remove()
this.patternEl.setAttribute('stroke', config.COLORS.BRIGHT)
}
/**
* Procedure for new patterns
* @return {Boolean} True if the pattern tested is correct
*/
checkPattern () {
let itsAmatch = this.onNewPattern(this.pattern)
this.isPendingReset = setTimeout(this.reset.bind(this), 1000)
// for (let i = this.patternEl.childNodes.length - 1; i >= 0; i--)
this.patternEl.setAttribute('stroke', itsAmatch ? config.COLORS.SUCCESS : config.COLORS.ERROR)
return itsAmatch
}
/*
* Drawn pattern
*/
/**
* Start a new 'current line'.
* The current line is the one in progress of being
* manipulated.
* @param {int} x Position X of the line starting point
* @param {int} y Position Y of the line starting point
*/
startLine (x, y) {
this.currentLine = dom.create('line', {
x1: x,
y1: y
})
this.patternEl.appendChild(this.currentLine)
}
/**
* Update the line of the current move
* @param {int} x Position X on the finger on the SVG scale
* @param {int} y Position Y on the finger on the SVG scale
*/
updateLine (x, y) {
if (!this.currentLine)
return
this.currentLine.setAttribute('x2', x)
this.currentLine.setAttribute('y2', y)
}
/**
* Update and close the line of the current move
* @param {int} x Position X of the line ending point
* @param {int} y Position Y of the line ending point
*/
closeLine (x, y) {
this.updateLine(x, y)
this.currentLine = null
}
}
export default LockCtrl

View File

@ -1,19 +0,0 @@
@import "../../_variables.scss";
.lock {
display: block;
width: 320px;
max-width: 90%;
margin: auto;
&-flashdots circle {
visibility: hidden;
opacity: 1;
&.active {
visibility: visible;
opacity: 0;
transition: visibility 0s, opacity .3s;
}
}
}

View File

@ -1,126 +0,0 @@
import ExtenderCtrl from '../extender/extender.ctrl'
import OptionCtrl from '../option/option.ctrl'
import SelectorCtrl from '../selector/selector.ctrl'
import config from '../../config'
import dom from '../../utils/dom'
import airportText from '../../utils/airportText'
require('./menu.scss');
/**
* Menu Controller
* Where all the magic starts!
* The welcoming screen with all the different settings
* to know what game to sart.
*/
class MenuCtrl {
/**
* ¯\_()_/¯
*/
constructor (onStart) {
this.onStart = onStart
this.setupTemplate()
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
let title = dom.create('h1', 'menu-title highlight unselectable', 'BreakLock'),
intro = dom.create('p', 'menu-intro', 'A hybrid of Mastermind and the Android pattern lock. A game you gonna love to hate.')
this.title = title;
this.typeHelpEl = dom.create('p', {}, 'Future info about the challenge')
this.btnStarlEl = dom.create('button', 'action-btn', 'START_')
airportText(title, 'BreakLock')
let instructions = new ExtenderCtrl('INSTRUCTIONS', document.getElementById('instructions-template'))
instructions.init()
// Options
this.difficultyOption = new OptionCtrl([
{ value: config.GAME.DIFFICULTY.EASY, label: 'Easy', default: true},
{ value: config.GAME.DIFFICULTY.MEDIUM, label: 'Medium'},
{ value: config.GAME.DIFFICULTY.HARD, label: 'Hard'}
])
this.typeSelector = new SelectorCtrl([
{
value: config.GAME.TYPE.PRACTICE,
label: 'Practice',
description: 'No pressure, just discover and practice your game',
default: true
},
{
value: config.GAME.TYPE.CHALLENGE,
label: 'Challenge',
description: 'Challenge mode give you 10 attempts only to win'
},
{
value: config.GAME.TYPE.COUNTDOWN,
label: 'Countdown',
description: 'Solve the game in one minute, without limit of attempts'
}
])
this.el = dom.create('div', 'menu-layout view', [
dom.create('div', 'view-bloc menu-layout-instructions', [
title,
intro,
instructions.el
]),
dom.create('div', 'view-bloc menu-layout-form', [
this.difficultyOption.el,
this.typeSelector.el,
this.typeHelpEl,
this.btnStarlEl
])
])
return this.el
}
/**
* Set up listeners
*/
init () {
this.typeSelector.init()
this.typeSelector.onSelect(this.typeChange.bind(this))
this.btnStarlEl.addEventListener('click', this.start.bind(this))
this.title.addEventListener('dblclick', this.triggerEasterEgg.bind(this))
}
/**
* Start a new game by calling the callback
* provided in the controller.
*/
start () {
this.onStart(this.typeSelector.getValue(), this.difficultyOption.getValue())
}
/**
* Selector for new type
* @param {object} type New selected type
*/
typeChange (type) {
this.typeHelpEl.textContent = type.description
}
/**
* Double click listener to trigger the OLED
* screen mode for a deep black design.
*/
triggerEasterEgg () {
if (localStorage.getItem('isDeepBlack')) {
localStorage.setItem('isDeepBlack', '')
document.body.classList.remove('deepblack')
}
else {
localStorage.setItem('isDeepBlack', 'on')
document.body.classList.add('deepblack')
}
}
}
export default MenuCtrl

View File

@ -1,26 +0,0 @@
@import "../../_variables.scss";
.menu-title {
margin: 0;
}
.menu-layout-instructions {
-webkit-flex:1 0 auto;
}
.menu-layout-form {
max-width: 480px;
}
.introduction-demo {
display: block;
width: 100%;
max-width: 320px;
margin: 1rem auto;
}
@media (orientation: landscape), (min-width: $mq-xlarge-min) {
.menu-layout {
align-items: center;
}
}

View File

@ -1,100 +0,0 @@
import dom from '../../utils/dom'
require('./option.scss');
/**
* Option Controller
* Component to build a one line selector
* between different options
* The class used for the parent element
* is .option, all childs are .option-item
*/
class OptionCtrl {
/**
* Setup the template and different options
* See `setChoices` method to understand the
* format of the following parameters
* @param {Array} choiceList List of key/values to display
*/
constructor (choiceList, defaultChoice) {
this.setupTemplate()
this.setChoices(choiceList)
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
this.el = dom.create('div', 'selectbox')
return this.el
}
/**
* Set up the different available choices
* Choice list format:
* [
* { value: <int>, label: <string>, default: <boolean> },
* { value: 2, label: 'Easy'},
* { value: 3, label: 'Medium', default: true},
* { value: 4, label: 'Hard'},
* ]
* @param {Array} choiceList List of options to display
*/
setChoices (choiceList) {
let listener = this.selectListener.bind(this)
choiceList.forEach((choice, index) => {
let option = dom.create('span', {
class: 'selectbox-item',
rel: choice.value
}, choice.label)
option.addEventListener('click', listener)
option.addEventListener('touchstart', listener)
this.el.appendChild(option)
if (choice.default)
this.selectFromTag(option)
return option
})
this.el.classList.add('selectbox-' + choiceList.length)
}
/**
* Listener for click on items
* @param {Event} event Event catched
*/
selectListener (e) {
e.preventDefault()
e.stopPropagation()
this.selectFromTag(e.currentTarget)
}
/**
* Update the selected value from the tag (:item)
* provided in parameter. The call will apply the
* class selected to the new tag (and remove it to
* the previous one), then also update the selected
* value of the instance.
* @param {DOMElement} tag Element tag selected
*/
selectFromTag (tag) {
if (this.selectedTag)
this.selectedTag.classList.remove('active')
this.selectedTag = tag
this.selectedTag.classList.add('active')
this.selectedValue = window.parseInt(tag.getAttribute('rel'), 10)
}
/**
* Return the current choice selected
* @return {int}
*/
getValue () {
return this.selectedValue
}
}
export default OptionCtrl

View File

@ -1,28 +0,0 @@
@import "../../_variables.scss";
.selectbox {
border: 1px solid $theme_bright;
margin-bottom: .5rem;
border-radius: .4rem;
padding: .25rem;
}
.selectbox-item {
@extend %button-feel;
padding: .25rem;
text-align: center;
display: inline-block;
box-sizing: border-box;
}
.selectbox-item.active {
background: $theme_bright;
color: $theme_dark;
border-radius: .2rem;
}
.selectbox-1 .selectbox-item { width: 100%; }
.selectbox-2 .selectbox-item { width: 50%; }
.selectbox-3 .selectbox-item { width: 33.33%; }
.selectbox-4 .selectbox-item { width: 25%; }
.selectbox-5 .selectbox-item { width: 20%; }

View File

@ -1,29 +0,0 @@
# Components
No framework has been used to make the app as lightweight as possible. However there's a lot of constraints. It's a bit more complex to extend, devs must be confortable with the DOM API, respect a structure.
Because this project is quite small, it was possible.
## Contructor
Every contructor will require different parameters, depending on what's required to build the component. But in every case, the constructor will build the DOM of the component.
## Properties
### `.el` [DOM Element]
This is the root DOM element of your component. It can be used to append your component anywhere else.
## Methods
### `.setupTemplate()`
Build the DOM of the component and set it to the `.el` property.
### `.init()`
Start to listen to required events. This method must be called manually, it's not triggered by default.
### `.destroy()`
Stop listening to events, delete the DOM and remove any link that could block the garbage collector to clean this up. However this method is not used at any point in this project.

View File

@ -1,130 +0,0 @@
import dom from '../../utils/dom'
require('./selector.scss');
/**
* Selector Controller
* Component to build a one line selector
* between different options via arrows
* The class used for the parent element
* is .selector, all childs are .selector-item
*/
class SelectorCtrl {
/**
* Setup the template and different options
* See `setChoices` method to understand the
* format of the following parameters
* @param {Array} choiceList List of key/values to display
*/
constructor (choiceList, defaultChoice) {
this.selectionIndex = 0
this.setupTemplate()
this.setChoices(choiceList)
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
this.btnLeft = dom.create('span', 'selectbox-item active selector-left', '<')
this.btnRight = dom.create('span', 'selectbox-item active selector-right', '>')
this.labelEl = dom.create('span', 'selectbox-item selector-label')
this.el = dom.create('div', 'selector selectbox', [
this.btnLeft,
this.btnRight,
this.labelEl
])
return this.el
}
/**
* Set up listeners
*/
init () {
this.btnLeft.addEventListener('click', this.previous.bind(this))
this.btnLeft.addEventListener('touchstart', this.previous.bind(this))
this.btnRight.addEventListener('click', this.next.bind(this))
this.btnRight.addEventListener('touchstart', this.next.bind(this))
}
/**
* Set up the different available choices
* Choice list format:
* [
* { value: <int>, label: <string>, default: <boolean> },
* { value: 2, label: 'Easy'},
* { value: 3, label: 'Medium', default: true},
* { value: 4, label: 'Hard'},
* ]
* @param {Array} choiceList List of options to display
*/
setChoices (choiceList) {
this.choices = choiceList
for (let i = this.choices.length - 1; i >= 0; i--) {
this.selectionIndex = this.choices[i].default ? i : this.selectionIndex
}
this.selectionIndex = this.selectionIndex || 0
this.updateLabel()
}
/**
* Update the counter DOM to the value
* set in the instance
* @return {int} The new value of the counter
*/
updateLabel () {
this.selectionIndex = (this.selectionIndex + this.choices.length) % this.choices.length
let choice = this.choices[this.selectionIndex]
this.labelEl.textContent = choice.label
if (this.selectCallback)
this.selectCallback(this.choices[this.selectionIndex])
return this.selectionIndex
}
/**
* Decrement the counter
* @return {int} The new value of the counter
*/
next (e) {
e.preventDefault()
e.stopPropagation()
this.selectionIndex++
return this.updateLabel()
}
/**
* Increment the counter
* @return {int} The new value of the counter
*/
previous (e) {
e.preventDefault()
e.stopPropagation()
this.selectionIndex--
return this.updateLabel()
}
/**
* Listener for when a new item is selected
* The listener will be called with only one
* parameter: the new selected value
* @param {function} listener Select listener to set
*/
onSelect (listener) {
this.selectCallback = listener
this.updateLabel()
}
/**
* Return the current choice selected
* @return {int}
*/
getValue () {
let choice = this.choices[this.selectionIndex]
return choice && choice.value
}
}
export default SelectorCtrl

View File

@ -1,18 +0,0 @@
@import "../../_variables.scss";
.selector-left, .selector-right {
width: 2rem;
}
.selector-left {
float: left;
}
.selector-right {
float: right;
}
.selector-label {
margin: 0 auto;
display: block;
}

View File

@ -1,129 +0,0 @@
import CountdownCtrl from '../countdown/countdown.ctrl'
import leftPadNum from '../../utils/leftPadNum'
import dom from '../../utils/dom'
require('./statusBar.scss');
/**
* Status Bar Controller
* Status bar of the current game in progress.
* It allows to display 2 different modes:
* - counter:
* showing only an integer that can be
* incremented or decremented
* - countdown:
* showing a progress bar as a timer
* The class used for the parent element
* is .status-bar
*/
class StatusBarCtrl {
/**
* Constructor
* The only callback provided is for cancelling
* event from the user.
* @param {function} onCancel Cancel callback
*/
constructor (onCancel) {
this.cancelCallback = onCancel
this.counterVal = null
this.setupTemplate()
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
this.cancelBtnEl = dom.create('button', 'status-bar-cancel', 'ABORT')
this.counterEl = dom.create('span', 'status-bar-info')
this.countdown = new CountdownCtrl()
this.countdownEl = this.countdown.el
this.countdownEl.setAttribute('class', 'status-bar-info')
this.el = dom.create('div', 'status-bar', [
this.cancelBtnEl,
this.counterEl,
this.countdownEl
])
return this.el
}
/**
* Set up listeners
*/
init () {
this.cancelBtnEl.addEventListener('click', () => {
this.cancelCallback(0)
})
}
/* Counter mode ******************************/
/**
* Set the counter mode to the status bar.
* The count is the value displayed on the
* counter.
* @param {int} attemptCount Counter value to display
*/
setCounter (count) {
this.counterEl.style.display = 'inherit'
this.countdownEl.style.display = 'none'
this.counterVal = count
this.updateCounter()
}
/**
* Update the counter DOM to the value
* set in the instance
* @return {int} The new value of the counter
*/
updateCounter () {
this.counterEl.textContent = leftPadNum(this.counterVal, 3)
return this.counterVal
}
/**
* Decrement the counter
* @return {int} The new value of the counter
*/
decrementCounter () {
this.counterVal--
return this.updateCounter()
}
/**
* Increment the counter
* @return {int} The new value of the counter
*/
incrementCounter () {
this.counterVal++
return this.updateCounter()
}
/* Countdown mode ****************************/
/**
* Set the countdown mode to the status bar
* @param {int} duration Duration in seconds
*/
setCountdown (duration) {
this.counterEl.style.display = 'none'
this.countdownEl.style.display = 'inherit'
this.countdown.setTimer(duration, () => {
this.cancelCallback(1)
})
this.countdown.start()
}
/**
* Stops the countdown
*
*/
stopCountdown () {
this.countdown.stop()
}
}
export default StatusBarCtrl

View File

@ -1,26 +0,0 @@
@import "../../_variables.scss";
.status-bar {
height: 1.5rem;
padding-bottom: .25rem;
border-bottom: 1px dotted $theme_bright;
&:after {
content: '';
clear: both;
}
&-cancel {
height: 100%;
font-size: 1rem;
&:before {
content: '«';
margin-right: .25em;
}
}
&-info {
float: right;
}
}

View File

@ -1,142 +0,0 @@
import summaryFeedback from './summaryFeedback'
import config from '../../config'
import Pattern from '../../models/pattern'
import PatternSVG from '../../utils/patternSVG'
import dom from '../../utils/dom'
import airportText from '../../utils/airportText'
require('./summary.scss');
/**
* Summary Controller
* End of game screen. The one that tell
* if the game was a success or not.
* It provide different actions and social
* button to promote the game.
*/
class SummaryCtrl {
/**
* Set up the template and init event.
* The constructor take one parameter, the callback
* for the following step.
* @param {function} onAction Action callback
*/
constructor (onAction) {
this.onAction = onAction
this.setupTemplate()
this.init()
}
/**
* Build template of the controller
* @return {DOMElement}
*/
setupTemplate () {
// Action buttons
this.actionButtons = []
for (let action in config.GAME.ACTIONS) {
let btn = dom.create('button', {
class: 'summary-action-button',
rel: config.GAME.ACTIONS[action]
}, [
dom.icon(action.toLowerCase()),
dom.create('span', {}, action)
])
this.actionButtons.push(btn)
}
// Social links
this.socialButtons = []
for (let platform in config.SOCIAL.PLATFORMS) {
let btn = dom.create('a', {
class: 'summary-share-link',
rel: 'noopener noreferrer',
target: '_blank',
platform
}, [dom.icon(config.SOCIAL.PLATFORMS[platform].ICON)])
this.socialButtons.push(btn)
}
// Feedback stuff
let feedbackEl = dom.create('div', 'summary-feedback bloc', [
dom.create('p', {}, [
dom.create('span', {}, 'Tweet me your feedback at '),
dom.create('a', {href: config.SOCIAL.PLATFORMS.TWITTER.URL('', '@mxwllt', ['breaklock'])}, '@mxwllt')
])
])
this.titleEl = dom.create('h1', 'summary-title highlight')
this.detailsEl = dom.create('p', 'summary-details')
this.revealEl = dom.create('p', 'summary-reveal', 'Continue to see the solution.')
this.actionsEl = dom.create('div', 'summary-actions bloc', this.actionButtons)
this.socialEl = dom.create('div', 'summary-share bloc', this.socialButtons)
this.el = dom.create('div', 'summary view', [
dom.create('div', 'view-bloc', [this.titleEl, this.detailsEl, this.revealEl]),
dom.create('div', 'view-bloc', [this.actionsEl, this.socialEl, feedbackEl])
])
return this.el
}
/**
* Set up listeners
*/
init () {
this.actionButtons.forEach(btn => btn.addEventListener('click', this.triggerAction.bind(this)))
}
/**
* Set new content.
* This is independent from the constructor,
* because an instance must be reused.
* @param {Boolean} isSuccess Was the game a success?
* @param {Number} attemptsCount Message to display
*/
setContent (isSuccess, attemptsCount) {
this.titleEl.classList.remove('fail')
this.titleEl.classList.remove('success')
this.titleEl.classList.add(isSuccess ? 'success' : 'fail')
airportText(this.titleEl, isSuccess ? 'Success!' : 'Fail!')
this.detailsEl.textContent = summaryFeedback(isSuccess, attemptsCount)
this.revealEl.classList[isSuccess ? 'add' : 'remove']('hide')
this.updateSocialLinks()
this.toggle(true)
}
/**
* Show/hide the controller
* @param {Boolean} force Force to show or hide if provided
*/
toggle (force) {
force = (force != undefined) ? force : !this.el.classList.contains('active')
this.el.classList[force ? 'add' : 'remove']('active')
}
/**
* Click listener for action buttons
* @param {Event} event Click event from action button
*/
triggerAction (event) {
let actionId = parseInt(event.currentTarget.getAttribute('rel') || 0, 10)
this.onAction(actionId)
}
/**
* Update social links from the current content set
*/
updateSocialLinks () {
this.socialButtons.forEach(item => {
let socialId = item.getAttribute('platform'),
socialObj = config.SOCIAL.PLATFORMS[socialId]
item.setAttribute('href', socialObj.URL(config.URL, config.SOCIAL.MESSAGE, config.SOCIAL.TAGS))
})
}
}
export default SummaryCtrl

View File

@ -1,76 +0,0 @@
@import "../../_variables.scss";
$summary_transition_duration: .5s;
$summary_transition_delay: .5s;
.summary {
position: fixed;
top: 0;
bottom: auto;
left: 0;
right: 0;
height: 0;
overflow: hidden;
background: $theme_dark;
visibility: hidden;
transition: visibility 0s linear $summary_transition_duration, height $summary_transition_duration ease;
-webkit-transition: visibility 0s linear $summary_transition_duration, height $summary_transition_duration ease;
body.deepblack & {
background-color: #000;
}
&.active {
visibility: visible;
height: 100%;
transition: visibility 0s, height $summary_transition_duration ease $summary_transition_delay;
-webkit-transition: visibility 0s, height $summary_transition_duration ease $summary_transition_delay;
}
// Hacks :-/
&.view {
padding: 0;
.view-bloc {
padding: $gutter;
}
}
}
.summary-title {
margin: 0;
&.success {
color: $theme_bright;
background-color: $theme_success;
}
&.fail {
color: $theme_bright;
background-color: $theme_error;
}
}
.summary-action-button > * {
vertical-align: middle;
margin-right: .5em;
}
.summary-action-button {
display: block;
font-size: 1.5rem;
}
.summary-share {
font-size: 2rem;
line-height: 0;
&-link {
display: inline-block;
margin-right: .375em;
}
}
@media (orientation: landscape), (min-width: $mq-xlarge-min) {
.summary {
align-items: center;
}
}

View File

@ -1,56 +0,0 @@
const SUCCESS_QUOTES_LIST = [
{ min: 1, max: 3, text: 'That was pure luck, nothing else. Stop dreamin.'},
{ min: 2, max: 4, text: 'You got lucky, without staying up all night.'},
{ min: 1, max: 2, text: 'No merit. Absolutely none.'},
{ min: 2, max: 5, text: 'That was given on a golden plate.'},
{ min: 1, max: 4, text: 'Absolutely no synapse got used during that game.'},
{ min: 2, max: 5, text: 'Don\'t even dare to tweet your score.'},
{ min: 8, max: 10, text: 'Saperlipopette!! That was close.'},
{ min: 4, max: 8, text: 'Seems legit, with a bit of luck.'},
{ min: 7, max: 10, text: 'Pretty good!'},
{ min: 9, max: 10, text: 'But you made it!'},
{ min: 11, max: 50, text: 'Trying random patterns is not a strategy...'},
{ min: 11, max: 50, text: 'That was looooooooong.'},
{ min: 11, max: 50, text: 'At least you made it.'},
{ min: 11, max: 50, text: 'You must hate this game by now.'},
{ min: 11, max: 50, text: 'I hope you didn\'t cheat.'},
{ min: 41, max: 403, text: 'Your dedication is impressive.'},
{ min: 404, max: 404, text: 'Logic not found.'},
{ min: 405, max: 999, text: 'No comment.'}
]
const FAIL_QUOTES_LIST = [
'I believe there\'s some work to do.',
'Do you understand the game? Don\'t take it personnaly, I struggle to explain it.',
'One day you will make it...',
'It\'s not funny for you, but it is for me ;)',
'Don\'t stress, you will make it.',
'If you want to avoid battles, stay out of the grassy areas!',
'Even if you loose in battle, if you surpass what you\'ve done before, you have bested yourself.',
'TILT! Insert coin and try again!'
]
/**
* Get a random quote to provide an awful
* feedback to the player.
* @param {boolean} wasSuccess Type of quote to get
* @param {number} attemptsCount Quote score
* @return {string}
*/
function getQuote (wasSuccess, attemptsCount) {
let feedback, matches
if (wasSuccess) {
feedback = `Lock found in ${attemptsCount} attempts. `
matches = SUCCESS_QUOTES_LIST
.filter(quote => (quote.min <= attemptsCount && quote.max >= attemptsCount))
.map(quote => quote.text)
}
else {
feedback = 'Sorry, you didn\'t make it this time. '
matches = FAIL_QUOTES_LIST
}
return feedback + matches[Math.floor(matches.length * Math.random())]
}
export default getQuote

View File

@ -1,112 +0,0 @@
/**
* Pattern class
* a pattern is a object representation of a combinaison.
* The amount of dot is specified in the constructor, it's
* not linked to the class itself.
*
* For reference:
* 0 1 2
* 3 4 5
* 6 7 8
*
*/
class Pattern {
/**
* Set up a pattern with only
* the length of dots to link
* @param {Number} dotLength Length of the pattern
*/
constructor (dotLength) {
this.dotLength = dotLength
this.suite = []
}
/**
* Fill the current instance with random values
*/
fillRandomly () {
while (!this.isComplete()) {
this.addDot(Math.floor(Math.random() * 9))
}
}
/**
* Add point to the current pattern
* @param {int} dotIndex Dot index to add
* @return boolean True if successfully added
*/
addDot (dotIndex) {
// Test if the dot can be added
if (this.isComplete() || ~this.suite.indexOf(dotIndex))
return [];
// Test for potential median dot
let lastDot = this.suite[this.suite.length - 1],
medianDot = (lastDot + dotIndex) / 2
if (lastDot != undefined &&
medianDot >> 0 === medianDot &&
(lastDot%3) - (medianDot%3) === (medianDot%3) - (dotIndex%3) &&
Math.floor(lastDot/3) - Math.floor(medianDot/3) === Math.floor(medianDot/3) - Math.floor(dotIndex/3)) {
let addedPoints = this.addDot(medianDot)
if (!this.isComplete()) {
this.suite.push(dotIndex)
addedPoints.push(dotIndex)
}
return addedPoints
}
this.suite.push(dotIndex)
return [dotIndex]
}
/**
* Checks if the instance suite is complete
* @return {Boolean}
*/
isComplete () {
return this.suite.length >= this.dotLength
}
/**
* Checks if a dot is already in the pattern
* @param {int} dotIndex Index to check
* @return {boolean}
*/
gotDot (dotIndex) {
return ~this.suite.indexOf(dotIndex)
}
/**
* Compare a pattern with the current instance.
* The output will be an array of three values:
* [0]: Number of dots in the right place in the pattern
* [1]: Number of correct dots badly placed in the pattern
* [2]: Number of wrong dots
* @param {Pattern} pattern Pattern to compare
* @return {Array}
*/
compare (pattern) {
var goodPos = 0,
wrongPos = 0
for (let i = 0; i < this.dotLength; i++) {
if (this.suite[i] === pattern.suite[i])
goodPos++
for (let j = 0; j < this.dotLength; j++) {
if (this.suite[j] === pattern.suite[i])
wrongPos++
}
}
return [goodPos, wrongPos - goodPos, this.dotLength - wrongPos]
}
/**
* Reset the pattern by removing all the dots
*/
reset () {
this.suite = []
}
}
export default Pattern

View File

@ -1,186 +0,0 @@
@import "./_variables.scss";
@font-face {
font-family: "Roboto Mono";
font-weight: $font_weight;
src: url('assets/fonts/robotomono-light-webfont.woff2') format('woff2'),
url('assets/fonts/robotomono-light-webfont.woff') format('woff'),
url('assets/fonts/robotomono-light-webfont.ttf') format('truetype');
}
* {
font-family: 'Roboto Mono', Menlo, Consolas, 'Andale Mono', monospace;
font-weight: $font_weight;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html, body {
height: 100%;
}
body {
position: relative;
margin: 0 auto;
padding: $gutter;
max-width: 1200px;
box-sizing: border-box;
background: $theme_dark;
color: $theme_bright;
overscroll-behavior-y: none;
}
body.deepblack {
background-color: #000;
}
a {
color: inherit;
}
button {
@extend %button-feel;
font-weight: $font_weight;
background: none;
color: $theme_bright;
line-height: inherit;
font-size: inherit;
border: none;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
font-weight: $font_weight;
}
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: middle;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
.icon {
width: 1em;
height: 1em;
vertical-align: middle;
* {
fill: currentColor;
}
}
::-webkit-scrollbar {
display: none;
}
*:focus {
outline: none;
}
.bloc {
border-top: 1px dotted currentColor;
padding: .75rem 0;
}
.action-btn {
display: block;
text-align: center;
margin: 0 auto .25rem;
font-size: 1.25rem;
border: 1px solid $theme_bright;
padding: .25rem 1rem;
box-shadow: .25rem .25rem 0 0 $theme_bright;
border-radius: .4rem;
}
.highlight {
display: inline-block;
padding: 0 .5rem;
color: $theme_dark;
background-color: $theme_bright;
&:after {
content:'_';
animation: blink 1.2s infinite;
-webkit-animation: blink 1.2s infinite;
}
}
.hide {
display: none;
}
.unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Layout *********/
.view {
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
padding: $gutter;
overflow: scroll;
}
.view-bloc {
flex-grow: 1;
}
@media (orientation: landscape), (min-width: $mq-xlarge-min) {
.view {
flex-direction: row;
}
.view-bloc {
flex-basis: 50%;
max-height: 100%;
overflow-y: scroll;
& + & {
margin-left: $gutter/2;
}
}
}
@media (max-width: 320px) {
html {
font-size: 14px;
}
}
@media (min-width: $mq-xlarge-min) {
html {
font-size: 20px;
}
}
@keyframes blink {
0% {opacity: 1;}
50% {opacity: 1;}
50.01% {opacity: 0;}
100% {opacity: 0;}
}
@-webkit-keyframes blink {
0% {opacity: 1;}
50% {opacity: 1;}
50.01% {opacity: 0;}
100% {opacity: 0;}
}

View File

@ -1,122 +0,0 @@
/**
* AirportText
*
* Made to display text with transition
* With an effect of airport display
*/
var animStack = [],
ratioFrameRate = 3
/**
* Set up text animation on a dom element
* @param {DOMElement} element Element to animate
* @param {String} text Text to display in the element
*/
function airportText (element, text) {
let existingAnim = popAnim(element)
if (existingAnim)
cancelFakeNextFrame(existingAnim.nextFrame)
var newAnim = {
el: element,
counter: text.length * ratioFrameRate,
originalLength: text.length,
originalText: text,
nextFrame: null
}
updateDisplay(newAnim)
animStack.push(newAnim)
}
/**
* Find an anim object from an element and extract it
* from the anim stack
* @param {DOMElement} element Element to find
* @return {Object}
*/
function popAnim (element) {
for (let i = animStack.length - 1; i >= 0; i--) {
if (animStack[i].el === element)
return animStack.splice(i, 1)[0]
}
}
/**
* Set the next for animation
* @param {Object} anim Anim object to set
*/
function setNextFrame (anim) {
anim.nextFrame = requestFakeNextFrame(function () {
updateDisplay(anim)
})
}
/**
* Update the text of an anim object
* @param {Object} anim Anim object to update
* @return {[type]}
*/
function updateDisplay (anim) {
// Increase the counter
anim.counter -= 1;
if (anim.counter <= 0) {
// Wait to be ready
anim.el.textContent = anim.originalText
popAnim(anim.el)
return
}
var randomLength = Math.floor(anim.originalLength - anim.counter / ratioFrameRate);
anim.el.textContent = anim.originalText.substr(0, randomLength) + getRandomWord(Math.min(anim.originalLength - randomLength, 3));
setNextFrame(anim)
}
/**
* getRandomWord
* Get a random word (in lowercase)
*
* @param pLength int Length of the random word
* @return string Random word
*/
function getRandomWord (pLength) {
var toReturn = '';
var charList = 'abcdefghijklmnopqrstuvwxyz0123456789 _*%!?/\\|#@';
// Param tests
if (pLength <= 0) {
return toReturn;
}
// Generate word
for (let i = 0; i < pLength; i++) {
toReturn += charList.charAt(Math.floor(Math.random() * charList.length));
}
return toReturn;
}
/**
* Fake mock of requestAnimationFrame
* @param {Function} callback Function to call
* @return {Int}
*/
function requestFakeNextFrame (callback){
return window.setTimeout(callback, 60);
}
/**
* Cancel a planed call
* @param {Int} id Process ID of the planed call to execute
*/
function cancelFakeNextFrame (id){
return window.clearTimeout(id);
}
export default airportText

View File

@ -1,44 +0,0 @@
/**
* Set of tools to manipulate colors
*/
var color = {
/**
* Create gradients of grey
* Specify the color to start and end with plus
* the amount of intermediate steps.
* The method will return an array of string
* format hex string.
*
* Example:
* greydient('99', 'FF', 2)
* > ['#999999', '#bbbbbb', '#dddddd', '#ffffff']
* @param {Number|String} colorStart Start gradient color (: '99' or 153)
* @param {Number|String} colorEnd End gradient color (: 'FF' or 255)
* @param {Number} steps Amount of intermediate colors
* @return {Array} Color list
*/
greydient: (colorStart, colorEnd, steps = 0) => {
// Convert to number if hex string
colorStart = typeof colorStart === 'string' ? parseInt(colorStart, 16) : colorStart
colorEnd = typeof colorEnd === 'string' ? parseInt(colorEnd, 16) : colorEnd
// Linit to the spectrum
colorStart = Math.min(255, Math.max(0, colorStart))
colorEnd = Math.min(255, Math.max(0, colorEnd))
steps++
let output = [],
gap = (colorEnd - colorStart) / steps
for (let i = 0; i <= steps; i++) {
let grey = Math.round(colorStart + i * gap),
greyHex = grey.toString(16)
output.push('#' + greyHex + greyHex + greyHex)
}
return output
}
}
export default color

View File

@ -1,69 +0,0 @@
/**
* Set of tools to generate quickly DOM elements
*/
var dom = {
/**
* Most advanced method (of this object) to
* create a DOM element
* @param {String} nodeName Node type to create
* @param {Object|String} props Key/value of attributes to set
* @param {Array|String} content Text content if string or childnodes if array to include
* @return {DOMElement} Node generated
*/
create: (nodeName, props = {}, content = null) => {
var node
// Create the node
if (dom.SVG_ELEMENTS.indexOf(nodeName) === -1)
node = document.createElement(nodeName)
else
node = document.createElementNS(dom.SVG_NAMESPACE, nodeName)
// Set class or attributes
if (props.constructor === String)
node.setAttribute('class', props)
else
for (let propName in props)
node.setAttribute(propName, props[propName])
// Set content or child
if (content instanceof Array)
for (let i = 0; i < content.length; i++) {
node.appendChild(content[i])
}
else
node.textContent = content
return node
},
/**
* Generate SVG icon dom, using 'use' tags
* and the definitions in the index.html
* @param {String} name Icon name (cf. definitions in index.html)
* @return {SVGDOMElement}
*/
icon: (name) => {
let use = dom.create('use')
use.setAttributeNS(dom.XLINK_NAMESPACE, 'href', '#icon-' + name)
return dom.create('svg', {class: 'icon'}, [use])
},
/**
* Clear the content of an element
* @param {DOMElement} element Element to clear
*/
clear: (element) => {
for (let i = element.childNodes.length - 1; i >= 0; i--) {
element.childNodes[i].remove()
}
},
SVG_NAMESPACE: 'http://www.w3.org/2000/svg',
XLINK_NAMESPACE: 'http://www.w3.org/1999/xlink',
SVG_ELEMENTS: ['svg', 'g', 'circle', 'line', 'path', 'use', 'rect']
}
export default dom

View File

@ -1,16 +0,0 @@
/**
* Dummy leftpad for numbers
* @param {number} num Number to left pad
* @param {number} len Length of the final string
* @return {string} Leftpadded string
*/
var leftPadNum = (num, len) => {
let str = Math.abs(num) + '',
isNegative = num < 0
for (let i = (len - str.length); i > 0; i--) {
str = '0' + str
}
return (isNegative ? '-' : '') + str
}
export default leftPadNum

View File

@ -1,154 +0,0 @@
import dom from './dom'
/**
* Class to generate SVG from Pattern objects
*/
class PatternSVG {
/**
* Setup the SVG base node
*
*/
constructor () {
this.el = dom.create('svg', {'viewBox': '0 0 ' + this.SVG_WIDTH + ' ' + this.SVG_WIDTH})
}
/**
* Add an invisible rectangle as background.
* This is only to help Safari to catch touch events
* on the SVG lock.
*/
addBackgroundLayer () {
let rect = dom.create('rect', {
'fill': '#fff',
'fill-opacity': '0',
'width': this.SVG_WIDTH,
'height': this.SVG_WIDTH
})
this.el.appendChild(rect)
return rect
}
/**
* Add pattern to the instance
* @param {Pattern} pattern Pattern instance to get the points from
* @param {int} size Thickness of the line
* @param {string|array} color List of colors to use for the pattern
* @return PatternSVG
*/
addPattern (pattern, size = 14, color = '#fff') {
let lines = []
color = color instanceof Array ? color : [color]
// Add all lines
for (let i = 1; i < pattern.suite.length; i++) {
lines.push(dom.create('line', {
'x1': (pattern.suite[i-1] % 3) * this.GRID_GUTTER + this.SVG_MARGIN,
'y1': Math.floor(pattern.suite[i-1] / 3) * this.GRID_GUTTER + this.SVG_MARGIN,
'x2': (pattern.suite[i] % 3) * this.GRID_GUTTER + this.SVG_MARGIN,
'y2': Math.floor(pattern.suite[i] / 3) * this.GRID_GUTTER + this.SVG_MARGIN,
'stroke': color[Math.min(color.length, i) - 1]
}))
}
// Add the dot reprenting the final dot
let lastDotIndex = pattern.suite[pattern.suite.length - 1]
lines.push(dom.create('circle', {
cx: (lastDotIndex % 3) * this.GRID_GUTTER + this.SVG_MARGIN,
cy: Math.floor(lastDotIndex / 3) * this.GRID_GUTTER + this.SVG_MARGIN,
fill: color[0],
r: size / 4
}))
// Bundle everything in a group
return this.addGroup({
'stroke-width': size,
'stroke-linecap': 'round'
}, lines)
}
/**
* Add dots to the instance
* @param {int} size Thickness of the line
* @param {object} attr List of attributes to set to the group
* @return PatternSVG
*/
addDots (size = 3, attr = {}) {
let dots = []
attr.fill = attr.fill || '#fff'
for (let i = 0; i < 9; i++) {
dots.push(dom.create('circle', {
cx: (i % 3) * this.GRID_GUTTER + this.SVG_MARGIN,
cy: Math.floor(i / 3) * this.GRID_GUTTER + this.SVG_MARGIN,
rel: i,
r: size
}))
}
return this.addGroup(attr, dots)
}
/**
* Add group to the instance
* @param {object} attr List of attributes to set to the group
* @param {*} content Items as content
* @return SVGDOMElement
*/
addGroup (attr, content) {
let group = dom.create('g', attr, content)
this.el.appendChild(group)
return group
}
/**
* Add combinaison results
* @param {int} goodDots Amount of good dots
* @param {int} badPlacedDots Amount of badly placed dots
* @param {int} wrongDots Amount of wrong dots
*/
addCombinaison (goodDots, badPlacedDots, wrongDots) {
let totalDots = goodDots + badPlacedDots + wrongDots,
dot = Math.min(Math.floor(this.SVG_WIDTH / totalDots), this.SVG_COMB_EXP),
dotWidth = Math.floor(dot * .75),
dotGap = Math.floor(dot * .25),
xGap = dotWidth + dotGap,
xStart = Math.floor((this.SVG_WIDTH - (totalDots - 1) * xGap) / 2),
yStart = this.SVG_WIDTH + Math.floor(this.SVG_COMB_EXP / 2)
this.el.setAttribute('viewBox', '0 0 ' + this.SVG_WIDTH + ' ' + (this.SVG_WIDTH + this.SVG_COMB_EXP))
let dots = []
for (let i = 0; i < totalDots; i++) {
dots.push(dom.create('circle', {
'cx': xStart + i * xGap,
'cy': yStart,
'r': (dotWidth - this.DOT_BORDER) / 2,
'stroke-width': this.DOT_BORDER,
'fill': i < goodDots ? '#fff' : '#000',
'stroke': i < (goodDots + badPlacedDots) ? '#fff' : '#000',
'fill-opacity': i < goodDots ? '1' : '.25',
'stroke-opacity': i < (goodDots + badPlacedDots) ? '1' : '.25'
}))
}
return this.addGroup({}, dots)
}
/**
* Get the SVG
* @return SVGDOMElement
*/
getSVG () {
return this.el
}
}
PatternSVG.prototype.SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
PatternSVG.prototype.SVG_WIDTH = 100
PatternSVG.prototype.SVG_COMB_EXP = 20
PatternSVG.prototype.SVG_MARGIN = 15
PatternSVG.prototype.GRID_GUTTER = 35
PatternSVG.prototype.DOT_BORDER = 2
PatternSVG.prototype.DOT_MAGNET = 6
export default PatternSVG

View File

@ -1,42 +0,0 @@
/**
* Alias for `requestAnimationFrame` or
* `setTimeout` function for deprecated browsers.
*
*/
window.requestAnimationFrame = (function () {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(/* function */ callback){
return window.setTimeout(callback, 1000 / 60);
}
);
})();
/**
* Alias for `cancelAnimationFrame` or
* `cancelTimeout` function for deprecated browsers.
*
*/
window.cancelAnimationFrame = (function () {
return (
window.cancelAnimationFrame ||
window.webkitCancelAnimationFrame ||
window.mozCancelAnimationFrame ||
window.oCancelAnimationFrame ||
window.msCancelAnimationFrame ||
function(id){
return window.clearTimeout(id);
}
);
})();
var toExport = {
requestAnimationFrame: window.requestAnimationFrame,
cancelAnimationFrame: window.cancelAnimationFrame
}
export default toExport

View File

@ -1,45 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const extractSass = new ExtractTextPlugin({
filename: "[name].css",
disable: process.env.NODE_ENV === "development"
});
module.exports = {
context: path.resolve(__dirname, './src'),
entry: {
app: './app.js',
},
output: {
path: path.resolve(__dirname, './'),
filename: 'app.js',
},
module: {
rules: [
{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: { presets: ['es2015'] }
}],
},
{
test: /\.scss$/,
loader: extractSass.extract({
loader: [{
loader: "css-loader",
options: { url: false }
}, {
loader: "sass-loader"
}],
fallbackLoader: "style-loader"
})
}
]
},
plugins: [
extractSass
]
};