1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: MIT |
3 | |
4 | struct Piece { |
5 | // col/row position of the tile in the puzzle |
6 | pos-x: int, |
7 | pos-y: int, |
8 | // offset in pixel from the base position for the kicking animation |
9 | offset-x: length, |
10 | offset-y: length, |
11 | } |
12 | |
13 | struct Theme { |
14 | name: string, |
15 | window-background-color: brush, |
16 | game-background-color: brush, |
17 | game-use-background-image: bool, |
18 | game-border: length, |
19 | game-radius: length, |
20 | game-text-color: color, |
21 | game-highlight-color: color, |
22 | piece-border: length, |
23 | piece-background-1: brush, |
24 | piece-background-2: brush, |
25 | piece-border-color-1: brush, |
26 | piece-border-color-2: brush, |
27 | piece-text-color-1: color, |
28 | piece-text-color-2: color, |
29 | piece-text-weight-incorrect-pos: int, |
30 | piece-text-weight-correct-pos: int, |
31 | piece-text-font-family: string, |
32 | piece-radius: length, |
33 | /// Ratio of the piece size |
34 | piece-spacing: float, |
35 | } |
36 | |
37 | component Checkbox inherits Rectangle { |
38 | in property <color> checked-color; |
39 | in property <color> unchecked-color; |
40 | in-out property <bool> checked; |
41 | |
42 | callback toggled(bool); |
43 | |
44 | states [ |
45 | /* pressed when ta.pressed : { |
46 | clip.width: root.width; |
47 | root.border-color: checked_color; |
48 | root.border-width: root.width; |
49 | }*/ |
50 | checked when root.checked : { |
51 | clip.width: root.width; |
52 | checkbox-rect.border-color: root.checked-color; |
53 | checkbox-rect.border-width: root.width; |
54 | in { |
55 | animate clip.width { duration: 200ms; easing: ease-in; } |
56 | animate checkbox-rect.border-width { duration: 100ms; easing: ease-out; } |
57 | } |
58 | out { |
59 | animate clip.width { duration: 100ms; easing: ease; } |
60 | animate checkbox-rect.border-width { duration: 200ms; easing: ease-in-out; } |
61 | animate checkbox-rect.border-color { duration: 200ms; easing: cubic-bezier(1,1,1,0); } |
62 | } |
63 | } |
64 | ] |
65 | |
66 | hover-rect := Rectangle { |
67 | background: #f5f5f5; |
68 | x: - parent.width / 4; |
69 | y: - parent.height / 4; |
70 | width: ta.has-hover ? root.width * 1.5 : 0px; |
71 | height: self.width; |
72 | border-radius: self.width; |
73 | } |
74 | |
75 | checkbox-rect := Rectangle { |
76 | border-width: self.height * 10%; |
77 | border-color: root.unchecked-color; |
78 | border-radius: 2px; |
79 | |
80 | clip := Rectangle { |
81 | x:0; |
82 | width: 0px; |
83 | clip: true; |
84 | |
85 | Text { |
86 | x:0;y:0; |
87 | width: root.width; |
88 | height: root.height; |
89 | text: "✓" ; |
90 | font-size: self.height * 80%; |
91 | color: white; |
92 | vertical-alignment: center; |
93 | horizontal-alignment: center; |
94 | |
95 | animate color { duration: 200ms; } |
96 | } |
97 | } |
98 | |
99 | ta := TouchArea { |
100 | clicked => { |
101 | root.checked = !root.checked; |
102 | root.toggled(root.checked); |
103 | } |
104 | } |
105 | |
106 | } |
107 | } |
108 | |
109 | import "./plaster-font/Plaster-Regular.ttf" ; |
110 | |
111 | export component MainWindow inherits Window { |
112 | in property <[Piece]> pieces: [ |
113 | { pos-x: 0, pos-y: 0 }, |
114 | { pos-x: 0, pos-y: 1 }, |
115 | { pos-x: 0, pos-y: 2 }, |
116 | { pos-x: 0, pos-y: 3 }, |
117 | { pos-x: 1, pos-y: 0 }, |
118 | { pos-x: 1, pos-y: 1 }, |
119 | { pos-x: 1, pos-y: 2 }, |
120 | { pos-x: 1, pos-y: 3 }, |
121 | { pos-x: 2, pos-y: 0 }, |
122 | { pos-x: 2, pos-y: 1 }, |
123 | { pos-x: 2, pos-y: 2 }, |
124 | { pos-x: 2, pos-y: 3 }, |
125 | { pos-x: 3, pos-y: 0 }, |
126 | { pos-x: 3, pos-y: 1 }, |
127 | { pos-x: 3, pos-y: 2 }, |
128 | ]; |
129 | out property <int> current-theme-index; |
130 | in-out property <bool> auto-play; |
131 | in-out property <int> moves; |
132 | in-out property <int> tiles-left; |
133 | |
134 | callback piece-clicked(int); |
135 | callback reset(); |
136 | callback enable-auto-mode(bool); |
137 | |
138 | private property <[Theme]> themes: [ |
139 | { |
140 | name: "SIMPLE" , |
141 | window-background-color: #ffffff, |
142 | game-background-color: #ffffff, |
143 | game-use-background-image: false, |
144 | game-border: 1px, |
145 | game-radius: 2px, |
146 | game-text-color: #858585, |
147 | game-highlight-color: #1d6aaa, |
148 | piece-border: 1px, |
149 | piece-background-1: #0d579b, |
150 | piece-background-2: #0d579b, |
151 | piece-border-color-1: #0a457b, |
152 | piece-border-color-2: #0a457b, |
153 | piece-text-color-1: #ffffff, |
154 | piece-text-color-2: #ffffff, |
155 | piece-text-weight-incorrect-pos: 400, |
156 | piece-text-weight-correct-pos: 700, |
157 | piece-radius: 5px, |
158 | /// Ratio of the piece size |
159 | piece-spacing: 10%, |
160 | }, |
161 | { |
162 | name: "BERLIN" , |
163 | window-background-color: #ffffff88, |
164 | game-background-color: #ffffffcc, |
165 | game-use-background-image: true, |
166 | game-border: 0px, |
167 | game-radius: 2px, |
168 | game-text-color: #858585, |
169 | game-highlight-color: #1d6aaa, |
170 | piece-border: 0px, |
171 | piece-background-1: #43689e, |
172 | piece-background-2: #2f2a14, |
173 | piece-border-color-1: #0000, |
174 | piece-border-color-2: #0000, |
175 | piece-text-color-1: #000000, |
176 | piece-text-color-2: #ffffff, |
177 | piece-text-weight-incorrect-pos: 700, |
178 | piece-text-weight-correct-pos: 700, |
179 | piece-radius: 0px, |
180 | /// Ratio of the piece size |
181 | piece-spacing: 8%, |
182 | }, |
183 | { |
184 | name: "PLASTER" , |
185 | window-background-color: #424244, |
186 | game-background-color: #f8f4e9, |
187 | game-use-background-image: false, |
188 | game-border: 5px, |
189 | game-radius: 20px, |
190 | game-text-color: #858585, |
191 | game-highlight-color: #e06b53, |
192 | piece-border: 4px, |
193 | piece-background-1: #e06b53, |
194 | piece-background-2: #f8f4e9, |
195 | piece-border-color-1: #424244, |
196 | piece-border-color-2: #e06b53, |
197 | piece-text-color-1: #f8f4e9, |
198 | piece-text-color-2: #424244, |
199 | piece-text-weight-incorrect-pos: 700, |
200 | piece-text-weight-correct-pos: 700, |
201 | piece-text-font-family: "Plaster" , |
202 | piece-radius: 5px, |
203 | /// Ratio of the piece size |
204 | piece-spacing: 10%, |
205 | }, |
206 | ]; |
207 | private property <Theme> current-theme: root.themes[root.current-theme-index]; |
208 | private property <length> pieces-size: min(root.width, root.height) / 6; |
209 | private property <length> pieces-spacing: root.current-theme.game-use-background-image && root.tiles-left == 0 ? |
210 | 2px : (root.pieces-size * root.current-theme.piece-spacing); |
211 | |
212 | |
213 | |
214 | title: "Slide Puzzle - Slint Demo" ; |
215 | |
216 | animate pieces-spacing { duration: 500ms; easing: ease-out; } |
217 | |
218 | Image { |
219 | // For the wasm build we want the puzzle to resize with the browser viewport, as per CSS in index.html. |
220 | // Our winit backend preserves the CSS set size if there's no preferred size set on the Slint window. |
221 | // This image propagates its preferred size and that means the window won't scale. By positioning it |
222 | // manually, the preferred size is ignored. |
223 | x: 0; y: 0; |
224 | height: 100%; width: 100%; |
225 | // https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0 |
226 | source: @image-url("berlin.jpg" ); |
227 | image-fit: cover; |
228 | } |
229 | |
230 | Rectangle { |
231 | background: root.current-theme.window-background-color; |
232 | animate background { duration: 500ms; easing: ease-out; } |
233 | } |
234 | |
235 | Rectangle { |
236 | background: root.current-theme.game-background-color; |
237 | border-color: root.current-theme.game-text-color; |
238 | border-width: root.current-theme.game-border; |
239 | border-radius: root.current-theme.game-radius; |
240 | width: root.pieces-size * 4.6; |
241 | height: root.pieces-size * 5.4; |
242 | x: (parent.width - self.width)/2; |
243 | y: (parent.height - self.height)/2; |
244 | animate background, border-color, border-width, border-radius { duration: 500ms; easing: ease-out; } |
245 | |
246 | Rectangle { |
247 | y:0; |
248 | width: parent.width * 90%; |
249 | height: root.pieces-size/2; |
250 | x: (parent.width - self.width) / 2; |
251 | |
252 | HorizontalLayout { |
253 | spacing: 0px; |
254 | |
255 | for theme[idx] in root.themes: TouchArea { |
256 | t := Text { |
257 | width: 100%; height: 100%; |
258 | text: theme.name; |
259 | color: idx == root.current-theme-index ? root.current-theme.game-highlight-color : root.current-theme.game-text-color; |
260 | vertical-alignment: center; |
261 | horizontal-alignment: center; |
262 | } |
263 | Rectangle { |
264 | background: t.color; |
265 | height: idx == root.current-theme-index ? 2px: 1px; |
266 | y: parent.height - self.height; |
267 | } |
268 | clicked => { |
269 | root.current-theme = theme; |
270 | root.current-theme-index = idx; |
271 | } |
272 | } |
273 | } |
274 | } |
275 | |
276 | |
277 | for p[i] in root.pieces : Rectangle { |
278 | property <float> px: p.pos-x; |
279 | property <float> py: p.pos-y; |
280 | property <bool> is-correct: i == p.pos-x * 4 + p.pos-y; |
281 | |
282 | x: self.py * (root.pieces-size + root.pieces-spacing) + p.offset-x |
283 | + (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2; |
284 | y: self.px * (root.pieces-size + root.pieces-spacing) + p.offset-y |
285 | + (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2; |
286 | width: root.pieces-size; |
287 | height: root.pieces-size; |
288 | drop-shadow-offset-x: 1px; |
289 | drop-shadow-offset-y: 1px; |
290 | drop-shadow-blur: 3px; |
291 | drop-shadow-color: #00000040; |
292 | border-radius: root.current-theme.piece-radius; |
293 | clip: true; |
294 | |
295 | states [ |
296 | pressed when touch.pressed : { |
297 | shadow.color: #0002; |
298 | circle.width: shadow.width * 2 * 1.4142; |
299 | in { |
300 | animate shadow.color { duration: 50ms; } |
301 | animate circle.width { duration: 2s; easing: ease-out; } |
302 | } |
303 | out { |
304 | animate shadow.color { duration: 50ms; } |
305 | } |
306 | } |
307 | hover when touch.has-hover: { |
308 | shadow.color: #0000000d; |
309 | } |
310 | ] |
311 | |
312 | animate px , py { duration: 170ms; easing: cubic-bezier(0.17,0.76,0.4,1.75); } |
313 | |
314 | if (root.current-theme.game-use-background-image) : Image { |
315 | height: 100%; width: 100%; |
316 | // https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0 |
317 | source: @image-url("berlin.jpg" ); |
318 | source-clip-x: mod(i, 4) * self.source.width / 4; |
319 | source-clip-y: floor(i / 4) * self.source.height / 4; |
320 | source-clip-width: self.source.width / 4; |
321 | source-clip-height: self.source.height / 4; |
322 | |
323 | if (root.tiles-left != 0) : Rectangle { |
324 | width: 60%; |
325 | height: 60%; |
326 | x: (parent.width - self.width) / 2; |
327 | y: (parent.height - self.height) / 2; |
328 | border-radius: self.width; |
329 | background: is-correct ? #0008 : #fff8; |
330 | } |
331 | } |
332 | |
333 | if (!root.current-theme.game-use-background-image) : Rectangle { |
334 | background: i >= 8 ? root.current-theme.piece-background-2 : root.current-theme.piece-background-1; |
335 | border-color: i >= 8 ? root.current-theme.piece-border-color-2 : root.current-theme.piece-border-color-1; |
336 | border-width: root.current-theme.piece-border; |
337 | border-radius: root.current-theme.piece-radius; |
338 | |
339 | animate border-width, border-radius { duration: 500ms; easing: ease-out; } |
340 | } |
341 | |
342 | if (!root.current-theme.game-use-background-image || root.tiles-left > 0) : Text { |
343 | text: i+1; |
344 | color: ((!root.current-theme.game-use-background-image && i >= 8) || (root.current-theme.game-use-background-image && is-correct)) ? root.current-theme.piece-text-color-2 : root.current-theme.piece-text-color-1; |
345 | font-size: root.pieces-size / 3; |
346 | font-weight: is-correct ? root.current-theme.piece-text-weight-correct-pos : root.current-theme.piece-text-weight-incorrect-pos; |
347 | font-family: root.current-theme.piece-text-font-family; |
348 | vertical-alignment: center; |
349 | horizontal-alignment: center; |
350 | width: 100%; |
351 | height: 100%; |
352 | } |
353 | |
354 | touch := TouchArea { |
355 | clicked => { root.piece-clicked(i); } |
356 | } |
357 | |
358 | shadow := Rectangle { |
359 | circle := Rectangle { |
360 | height: self.width; |
361 | border-radius: self.width/2; |
362 | background: #0002; |
363 | x: touch.pressed-x - self.width/2; |
364 | y: touch.pressed-y - self.width/2; |
365 | } |
366 | } |
367 | } |
368 | |
369 | if (root.tiles-left == 0) : Text { |
370 | width: root.pieces-size; |
371 | height: root.pieces-size; |
372 | x: 3 * (root.pieces-size + root.pieces-spacing) |
373 | + (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2; |
374 | y: 3 * (root.pieces-size + root.pieces-spacing) |
375 | + (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2; |
376 | |
377 | color: root.current-theme.game-highlight-color; |
378 | font-size: root.pieces-size / 2; |
379 | vertical-alignment: center; |
380 | horizontal-alignment: center; |
381 | text: "🖒" ; |
382 | |
383 | if (root.current-theme.game-use-background-image) : Image { |
384 | height: 100%; width: 100%; |
385 | // https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0 |
386 | source: @image-url("berlin.jpg" ); |
387 | source-clip-x: 3 * self.source.width / 4; |
388 | source-clip-y: 3 * self.source.height / 4; |
389 | source-clip-width: self.source.width / 4; |
390 | source-clip-height: self.source.height / 4; |
391 | } |
392 | } |
393 | |
394 | Rectangle { |
395 | width: parent.width; |
396 | height: 1px; |
397 | background: root.current-theme.game-text-color; |
398 | y: parent.height - root.pieces-size / 2; |
399 | } |
400 | |
401 | HorizontalLayout { |
402 | height: root.pieces-size / 2; |
403 | y: parent.height - root.pieces-size / 2; |
404 | width: parent.width; |
405 | padding: self.height * 25%; |
406 | |
407 | Text { |
408 | text: " ↻ " ; |
409 | font-size: parent.height * 40%; |
410 | color: root.current-theme.game-highlight-color; |
411 | vertical-alignment: center; |
412 | TouchArea { |
413 | clicked => { root.reset(); } |
414 | } |
415 | } |
416 | |
417 | Checkbox { |
418 | toggled(checked) => { root.enable-auto-mode(self.checked) } |
419 | |
420 | width: parent.height - 2 * parent.padding; |
421 | checked <=> root.auto-play; |
422 | checked-color: root.current-theme.game-highlight-color; |
423 | unchecked-color: root.current-theme.game-text-color; |
424 | } |
425 | |
426 | Rectangle {} // stretch |
427 | |
428 | Text { |
429 | text: root.moves; |
430 | color: root.current-theme.game-highlight-color; |
431 | vertical-alignment: center; |
432 | } |
433 | |
434 | Text { |
435 | text: "Moves " ; |
436 | color: root.current-theme.game-text-color; |
437 | vertical-alignment: center; |
438 | } |
439 | |
440 | Text { |
441 | text: root.tiles-left; |
442 | color: root.current-theme.game-highlight-color; |
443 | vertical-alignment: center; |
444 | } |
445 | |
446 | Text { |
447 | text: "Tiles left" ; |
448 | color: root.current-theme.game-text-color; |
449 | vertical-alignment: center; |
450 | } |
451 | } |
452 | } |
453 | } |
454 | |