') 100% no-repeat;background-color:#fff;box-shadow:0 .2rem .2rem .1rem hsla(0,0%,40%,.5);cursor:pointer;margin-bottom:1rem;padding-right:3.2rem}select:focus{border-color:#4c4355;box-shadow:0 0 .2rem .2rem rgba(76,76,76,.3),0 .2rem .2rem .1rem hsla(0,0%,40%,.5)!important}textarea{min-height:6.4rem}label,legend{color:#332c39;display:block;font-weight:600}fieldset{border-width:0;padding:0}input[type=checkbox],input[type=radio]{cursor:pointer;display:inline;margin-bottom:0}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:default}input[type=checkbox]+span,input[type=radio]+span{cursor:pointer;font-weight:400;width:100%}input[type=checkbox]:disabled+span,input[type=radio]:disabled+span,span.toggleLabel+input[type=checkbox]:disabled{color:#616161;cursor:default!important}.label-inline{color:#19161c;display:inline-block}.label-inline+input{display:inline-block;margin-left:1.2rem;margin-right:1.2rem;width:unset}label>input[type=checkbox]{align-content:center;align-items:center;display:inline-flex}@media (min-width:680px){input[type=checkbox]+span:before{height:1.4rem;width:1.4rem}}span input[type=checkbox]:disabled+{cursor:default!important}input[type=checkbox]{opacity:0;position:absolute}input[type=checkbox]+span{cursor:pointer;line-height:2.3rem;padding-left:2.8rem;position:relative}input[type=checkbox]:active+span:before{background-color:#ccc}input[type=checkbox]+span:before{background-color:#f5f4f2;border:.1rem solid #665972;border-radius:.1rem;bottom:.3rem;display:inline-block;height:1.7rem;left:0;margin-right:.8rem;vertical-align:text-top;width:1.7rem}input[type=checkbox]+span:after,input[type=checkbox]+span:before{content:"";position:absolute;transition:all .05s;user-select:none}input[type=checkbox]+span:after{bottom:1.1rem;box-shadow:.2rem 0 0 #f5f4f2,.4rem 0 0 #f5f4f2,.4rem -.6rem 0 #f5f4f2,.4rem -.8rem 0 #f5f4f2;height:.3rem;left:.2rem;transform:rotate(90deg) scale(0);transform-origin:5% 85%;width:.3rem}input[type=checkbox]:checked+span:after{background-color:#8c3e34;box-shadow:.2rem 0 0 #8c3e34,.4rem 0 0 #8c3e34,.4rem -.2rem 0 #8c3e34,.4rem -.4rem 0 #8c3e34,.4rem -.6rem 0 #8c3e34,.4rem -.8rem 0 #8c3e34;content:"";transform:rotate(45deg) scale(1)}input[type=checkbox]:indeterminate+span:after{background-color:#8c3e34;bottom:1.1rem;box-shadow:unset;height:1.2rem;left:.4rem;transform:rotate(90deg) scale(1);width:.3rem}input[type=checkbox]:focus+span:before{border-color:#4c4355;border-width:.2rem;box-shadow:0 0 .2rem .2rem rgba(76,76,76,.3)!important}input[type=checkbox]:disabled+span:before{background:#8c8c8c;border:.1rem solid #8c8c8c;box-shadow:none}input[type=checkbox]:disabled+span{cursor:default!important}input[type=checkbox]:disabled:checked+span:before{cursor:default}input[type=checkbox]:disabled:checked+span:after{background:#ccc;box-shadow:.2rem 0 0 #ccc,.4rem 0 0 #ccc,.4rem -.2rem 0 #ccc,.4rem -.4rem 0 #ccc,.4rem -.6rem 0 #ccc,.4rem -.8rem 0 #ccc;cursor:default}input[type=checkbox]:disabled:indeterminate+span:after{background-color:#ccc}input[type=checkbox].toggle-switch{opacity:0;position:absolute}input[type=checkbox].toggle-switch+span:after,input[type=checkbox].toggle-switch+span:before{box-shadow:none;margin:0 .8rem 0 0;transform:unset;vertical-align:unset}input[type=checkbox].toggle-switch+span{align-items:baseline;display:inline-flex;height:2.3rem;margin:0;padding-left:4.8rem;vertical-align:text-bottom}input[type=checkbox].toggle-switch+span:before{background:gray;border:none;border-radius:1.6rem;content:"";display:inline-block;height:1.6rem;width:3.2rem}input[type=checkbox].toggle-switch+span:after{background-color:#f5f4f2;border-radius:50%;bottom:.5rem;content:"";display:inline-block;height:1.2rem;left:.2rem;transition:left .14s cubic-bezier(.32,0,.67,0);width:1.2rem}input[type=checkbox].toggle-switch:active+span:before,input[type=checkbox].toggle-switch:focus+span:before{border-color:#998ca5;box-shadow:0 0 .2rem .2rem rgba(140,126,154,.6)!important}input[type=checkbox].toggle-switch:disabled+span:after{background:#ccc!important}input[type=checkbox].toggle-switch:disabled+span:before{background:#8c8c8c!important;box-shadow:none!important}input[type=checkbox].toggle-switch:checked+span:after,input[type=checkbox].toggle-switch:checked+span:before{box-shadow:none;transform:unset}input[type=checkbox].toggle-switch:checked+span:after{background-color:#f5f4f2;display:inline-block;left:1.7rem}input[type=checkbox].toggle-switch:checked+span:before{background:#b3675c!important}input[type=checkbox].toggle-switch:checked:disabled+span:after{background:#ccc}input[type=checkbox].toggle-switch:checked:disabled+span:before{background:#ababab!important}.toggleLabel{color:#19161c;position:relative;top:.2rem}.radio-inline,.toggleLabelAfter,.toggleLabelBefore{margin-right:.8rem}.radio-inline{display:inline-block;min-width:9.2rem}input[type=radio]:checked,input[type=radio]:not(:checked){left:-9999px;position:absolute}input[type=radio]:checked+span:before{background-color:#f5f4f2;border:.1rem solid #665972;border-radius:100%;content:"";height:1.6rem;left:0;position:absolute;top:.15rem;width:1.6rem}input[type=radio]:active+span:before{background-color:#ababab;border:none;box-shadow:none}input[type=radio]:focus:checked+span:before{border:.1rem solid #4c4355;box-shadow:0 0 .2rem .2rem rgba(76,67,85,.6)}input[type=radio]:checked+span,input[type=radio]:not(:checked)+span{cursor:pointer;display:inline-block;line-height:2.2rem;padding-left:2.4rem;position:relative}input[type=radio]:checked:disabled+span:before{border:.1rem solid #8c8c8c;cursor:default}input[type=radio]:not(:checked)+span:before{background-color:#f5f4f2;border:.1rem solid #665972;border-radius:100%;content:"";height:1.6rem;left:0;position:absolute;top:.15rem;width:1.6rem}input[type=radio]:not(:checked):active+span:before{background-color:#ababab;border:none;box-shadow:none}input[type=radio]:not(:checked):focus+span:before{border:.1rem solid #4c4355;box-shadow:0 0 .2rem .2rem rgba(76,67,85,.6)}input[type=radio]:not(:checked):disabled+span:before{background-color:#8c8c8c;border:none;cursor:default}input[type=radio]:checked+span:after,input[type=radio]:not(:checked)+span:after{background:#8c3e34;border-radius:100%;content:"";height:.6rem;left:.5rem;position:absolute;top:.65rem;transform:scale(1);transition:transform .14s;width:.6rem}input[type=radio]:not(:checked)+span:after{transform:scale(0);transition:transform .5s cubic-bezier(.33,1,.68,1)}input[type=radio]:checked:disabled+span:before{background:0 0}input[type=radio]:checked:disabled+span:after{background:#8c8c8c;cursor:default}input[type=radio]:disabled+span{color:#616161;cursor:default}input[type=radio]:checked+span:after{opacity:1;transform:scale(1)}input[type=range]{-webkit-appearance:none;background:0 0;margin-top:1.6rem;width:100%}input[type=range]:focus{background-color:initial;outline:0}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;background:#fff;border:.1rem solid #8a8a8a;border-radius:45%;box-shadow:0 .2rem .2rem .1rem hsla(0,0%,40%,.5);cursor:pointer;height:2rem;margin-top:-.8rem;outline:0;width:2rem}input[type=range]:focus::-webkit-slider-thumb{border:.1rem solid #4c2c6c;box-shadow:0 0 .2rem .2rem rgba(76,67,85,.6),0 .2rem .2rem .1rem hsla(0,0%,40%,.5)!important}input[type=range]:disabled::-webkit-slider-thumb{background-color:#8c8c8c;border:.1rem solid #ccc;box-shadow:none;cursor:default}input[type=range]::-moz-range-thumb{background:#fff;border:.1rem solid #8a8a8a;border-radius:45%;box-shadow:0 .2rem .2rem .1rem hsla(0,0%,40%,.5);cursor:pointer;height:2rem;margin-top:-.8rem;outline:0;width:2rem}input[type=range]:focus::-moz-range-thumb{border:.1rem solid #4c2c6c;box-shadow:0 0 .2rem .2rem rgba(76,67,85,.6),0 .2rem .2rem .1rem hsla(0,0%,40%,.5)!important}input[type=range]:disabled::-moz-range-thumb{background-color:#8c8c8c;border:.1rem solid #ccc;box-shadow:none;cursor:default}input[type=range]::-webkit-slider-runnable-track{background-color:#8a8a8a;border-radius:0;cursor:pointer;height:.6rem;outline:0;width:100%}input[type=range]:disabled::-webkit-slider-runnable-track{background-color:#ccc;border:.1rem solid #8c8c8c;cursor:default}input[type=range]::-moz-range-track{background-color:#8a8a8a;border-radius:0;cursor:pointer;height:.6rem;outline:0;width:100%}input[type=range]:disabled::-moz-range-track{background-color:#ccc;border:.1rem solid #8c8c8c;cursor:default}.container,.container-fw{margin:0 auto;padding:0 .8rem;position:relative;width:100%}.container-fw{max-width:100vw}.container{max-width:80rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.colGap1{column-gap:1.6rem}.row.colGap2{column-gap:3.2rem}.row.colGap3{column-gap:4.8rem}.row.colGap4{column-gap:6.4rem}.row.row-no-padding,.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;min-width:0;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width:680px){.container,.container-fw{padding:0 2.4rem}.row{flex-direction:row;margin-left:-1.2rem;width:calc(100% + 2.4rem)}.row .column{margin-bottom:inherit;padding:0 1.2rem}}dl,ol,ul{list-style:none;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{margin:.4rem 0 .8rem 3em}dl ul>li,ol ul>li,ul ul>li{list-style:circle outside}ol,ul{list-style:none;margin:.4rem 0 .8rem 3em;padding-left:0}ol ul>li{list-style:disc outside}ol{list-style:decimal outside}ul{list-style:disc outside}fieldset{padding:0!important}input[type=range]{padding-bottom:1.2rem}li{margin-bottom:.6rem}blockquote,dl,figure,ol,p,pre,ul{margin-bottom:1.2rem;margin-top:0}table{margin:2.4rem 0}@media (min-width:680px){li{margin-bottom:.4rem}}table{border-spacing:0;max-width:100%}thead{background-color:#ababab;color:#212121}td,th{border-bottom:.1rem solid #8a8a8a;line-height:1;padding:.8rem;text-align:left}td:first-child,th:first-child{padding-left:1.6rem}td:last-child,th:last-child{padding-right:1.6rem}th{border-bottom:0;font-weight:400}@media (min-width:680px){table{width:auto}}b,strong{font-weight:700}p{margin-top:0}h1,h2,h3,h4,h5,h6{color:#414141;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-weight:600;letter-spacing:-.1rem;line-height:1.2;margin:0 0 2.4rem}h1{font-size:3.6rem;font-weight:600;margin:3.2rem 0}h2{font-size:2.8rem;letter-spacing:0;line-height:1.25;margin:2rem 0}h3{font-size:1.8rem;font-weight:800}h3,h4{color:#0a0a0a;line-height:1.3}h4{font-size:1.6rem;text-decoration:underline}img{max-width:100%}figure{margin:0}video{display:block;margin:0 0 2.4rem;max-width:100%;outline:0}.clearfix:after{clear:both;content:" ";display:table}.float-left{float:left}.float-right{float:right}.cur-ptr{cursor:pointer}.disp-ib{display:inline-block!important}.disp-none{display:none!important}@media (max-width:679px){.show-sm-desktop{display:none!important}}@media (max-width:1279px){.show-lg-desktop{display:none!important}}@media (min-width:0px){.hide-mobile{display:none!important}}@media (min-width:680px){.hide-sm-desktop,.show-mobile{display:none!important}}@media (min-width:1280px){.hide-lg-desktop{display:none!important}}.pos-abs{position:absolute}.pl0{padding-left:0}.pl1{padding-left:.8rem}.pl2{padding-left:1.6rem}.pr0{padding-right:0}.pr1{padding-right:.8rem}.pr2{padding-right:1.6rem}.pb0{padding-bottom:0}.pb1{padding-bottom:.8rem}.pt0{padding-top:0}.pt1{padding-top:.8rem}.pt2{padding-top:1.6rem}.p0{padding:0}.p1{padding:.8rem}.p2{padding:1rem}.ml0{margin-left:0}.ml1{margin-left:.8rem}.mr0{margin-right:0}.mr1{margin-right:.8rem}.mr2{margin-right:1.6rem}.mt0{margin-top:0}.mt1{margin-top:.8rem}.mt2{margin-top:1.6rem}.mt3{margin-top:2.4rem}.mb0{margin-bottom:0}.mb1{margin-bottom:.8rem!important}.mb2{margin-bottom:1.6rem}.mb3{margin-bottom:2.4rem!important}.m0{margin:0}.mw100{max-width:100%}.m1{margin:.8rem}.m-center{margin:0 auto}.gpu-scrl{transform:translateZ(0)}.text-center{text-align:center}.text-right{text-align:right}.no-wrap{white-space:nowrap}.fsLarge{font-size:3rem}.fw-400{font-weight:400}.fw-500{font-weight:500}.fw-600{font-weight:600}.colorBlackLt{color:#4c4355}.ptrEvNone{pointer-events:none}.rootNavContainer{display:flex;flex-direction:column;margin-bottom:1.6rem}#root-nav{background:#ccc;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1.3rem;padding:1.2rem .4rem}#root-nav a{color:#8c3e34}#root-nav a:hover{color:#3e31a2}#root-nav ol{list-style:none;margin:0;padding-left:2.4rem}#root-nav ol li{display:inline-block;margin:0 .6rem 0 0;text-transform:capitalize}#root-nav ol li:first-child{margin-left:-2.4rem;padding-right:3.2rem;text-transform:none}#root-nav ol li:first-child:after,#root-nav ol li:last-child:after{content:""}#root-nav ol li:after{content:" /"}@media (min-width:680px){#root-nav{font-size:1.4rem}#root-nav ol li:first-child{padding-right:4rem}}a{color:#3e31a2;cursor:pointer;text-decoration:underline}a:focus,a:hover{color:#19161c}a:visited{color:#8c3e34}a:visited:focus,a:visited:hover{color:#3e31a2}body,html{height:100%}body{background-color:#ccc}.div__noscript{background-color:#8d47b3;color:#d0dc71;font-size:2.2rem}#container,#fakeContainer{background-color:#ccc;height:auto}.bg-c64-ltgreen{background-color:#acea88}.bg-c64-ltyellow{background-color:#d0dc71}.bg-c64-grey1{background-color:#ccc}.bg-c64-grey2{background-color:#ababab}.bg-c64-grey3{background-color:#8a8a8a}.bg-c64-ltblue{background-color:#7abfc7}.bg-c64-ltred{background-color:#bb776d}.bg-c64-green{background-color:#68a941}.bg-c64-ltpurple{background-color:#7c70da}.bg-c64-ltbrown{background-color:#905f25}.bg-c64-magenta{background-color:#8d47b3}.bg-c64-dkred{background-color:#8c3e34}.bg-c64-dkbrown{background-color:#574200}.bg-c64-dkpurple{background-color:#3e31a2}.colour-c64-ltgreen{color:#acea88}.colour-c64-ltyellow{color:#d0dc71}.colour-c64-grey1{color:#ccc}.colour-c64-grey2{color:#ababab}.colour-c64-grey3{color:#8a8a8a}.colour-c64-grey4{color:gray}.colour-c64-grey5{color:#676767}.colour-c64-grey6{color:#545454}.colour-c64-ltblue{color:#7abfc7}.colour-c64-ltred{color:#bb776d}.colour-c64-green{color:#68a941}.colour-c64-ltpurple{color:#7c70da}.colour-c64-ltbrown{color:#905f25}.colour-c64-magenta{color:#8d47b3}.colour-c64-dkred{color:#8c3e34}.colour-c64-dkbrown{color:#574200}.colour-c64-dkpurple{color:#3e31a2}
derekh.ca ( ) home making tetris with surplus 01 June 2018
Making Tetris With Surplus
Play tetris
Making a tetris clone has become my goto project when learning new languages/frameworks. This was mostly a port to Surplus of a clone written for React + Redux.
The weirdiest thing about the switch was probably figuring out Surplus lifecycles and rewriting state updates that were no longer asynchronous. There was a good drop in the bundle size (from 100 KB to 50 KB ) after the rewrite. I didn’t notice if there was a performance improvement/degradation, but Tetris doesn’t have very high requirements.
Managing State State is managed in Surplus by S.js using signals and S computations. When you evaluate a signal on an JSX component attribute, the Surplus compiler wraps the evaluation inside an S computation. These are used by the runtime to track dependencies and update the DOM in response to changes in state.
For the most part this maps nicely to how React’s functional components update. It’s slightly trickier creating stateful components because Surplus components are just syntactic sugar for function calls. This requires that you are more aware about the signals used by the surrounding context. This puts an additional burden on the programmer, but you also need to take care of unnecessary renders in React, though the component lifecycle can at least let you separate mounts from updates unlike Surplus.
The following is a small example of how the evaulation of a signal can affect a component’s lifecycle in Surplus.
let count = S.data (0 );
...
<MyComponent counts={count ()} />
let count = S.data (0 );
...
<MyComponent counts={count} />
I found the first pattern useful as a natural way to control the lifetime of certain components. In the game, there’s internal state used to track animations on the ControlledTetra
component. This component is used to draw the current tetra piece. One of the props passed to this component is a signal that stores game state about the current tetra piece. This signal is evaluated as an attribute like the second example above to recreate a new instance (DOM and JS) whenever the controlled tetra piece is swapped or otherwise replaced. This was a nice way to ensure there was no leftover animation state from the previous tetra piece.
Unexpected Infinite Cycling One of the more annoying things I would do is accidentally create a cycle between two S.js DataSignals causing S.js to get stuck in an endless update spiral. Combined with logging output, it’s a great way to lock up the page. It was pretty frustrating to see a blank page and then an absolute dump of info once the dev tools was opened. Pretty much had to keep the Chrome Task Manager open all the time.
Replacement Callbacks For DOM Manipulation In order for things like FLIP to work you need to access the DOM before relayout (or exactly after). React’s componentDidMount
and componentDidUpdate
callbacks are useful here because they are called in a single execution block after the DOM is updated. This can allow low level DOM modification needed before the layout is updated. Surplus doesn’t have lifecycles, so these callbacks need to be added manually.
One option for a replacement componentDidMount
is put a function in the DOM itself ensuring it executes when the DOM is ready as well as during the same execution block. It’s important that no dependencies on signals are created in that function so that onMount
is not called again.
<div>
<div ref ={divRef} >
<label > Bla</label >
More bla
</div >
{onMount ()}
</div>
componentDidUpdate
can be more interesting. The most common scenario would be to pass a reference to a DataSignal and then create another DataSignal that will shadow its value.
function UpdateComponent ({signal} ){
let innerSig = S.data (S.sample (signal));
S.on (signal,()=> {
innerSig (signal ());
});
return <div > {innerSig()}</div >
}
The example is a little contrived, but you could now add other properties to that component without its lifecycle being affected by changes to signal. What I like about Surplus is that you can just use signal
directly in the JSX of UpdateComponent
and the inner JSX will just update without tearing down the entire component. In fact, in many cases there’s no reason to create additional state like we would have done with React. Especially when we can add helper functional components to contain complex DOM operations related to that signal.
Reference www.colinfahey.com Excellent Tetris reference. Absurd amounts of info about Tetris, its history, implementation, and more.