Initial commit
@@ -0,0 +1,52 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: stable
|
||||||
|
|
||||||
|
- name: Root pub get
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Example pub get
|
||||||
|
working-directory: example
|
||||||
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: dart format --output=none --set-exit-if-changed .
|
||||||
|
|
||||||
|
- name: Analyze root
|
||||||
|
run: flutter analyze
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: flutter test
|
||||||
|
|
||||||
|
- name: Outdated root
|
||||||
|
run: flutter pub outdated
|
||||||
|
|
||||||
|
- name: Outdated example
|
||||||
|
working-directory: example
|
||||||
|
run: flutter pub outdated
|
||||||
|
|
||||||
|
- name: Dartdoc dry-run
|
||||||
|
run: dart doc --dry-run
|
||||||
|
|
||||||
|
- name: Publish dry-run
|
||||||
|
run: dart pub publish --dry-run
|
||||||
|
|
||||||
|
- name: Pana
|
||||||
|
run: |
|
||||||
|
dart pub global activate pana
|
||||||
|
pana .
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
- platform: android
|
||||||
|
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "shiny",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shiny (profile mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "shiny (release mode)",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example",
|
||||||
|
"cwd": "example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example (profile mode)",
|
||||||
|
"cwd": "example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "profile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "example (release mode)",
|
||||||
|
"cwd": "example",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "dart",
|
||||||
|
"flutterMode": "release"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0
|
||||||
|
|
||||||
|
- Initial release of `holo_shiny`.
|
||||||
|
- Added `Shiny` widget with shader-driven holographic effects.
|
||||||
|
- Added optional `ShinyController` and `SensorTiltController` motion APIs.
|
||||||
|
- Added package example app and baseline tests.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# holo_shiny
|
||||||
|
|
||||||
|
`holo_shiny` is a Flutter package that renders shader-driven holographic shine.
|
||||||
|
|
||||||
|
It exposes two widget layers:
|
||||||
|
|
||||||
|
- `Shiny`: apply shine to any widget (no clipping and no rotation)
|
||||||
|
- `ShinyCard`: card-oriented convenience widget with shape clipping and tilt rotation
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- GLSL shader-driven holographic effects
|
||||||
|
- Optional motion input via sensors or custom tilt stream
|
||||||
|
- Generic wrapper API for any widget via `Shiny(child: ...)`
|
||||||
|
- Card API with `background`, `foreground`, `shape`, and drag tilt via `ShinyCard`
|
||||||
|
- Built-in sparkle presets including 8-point star, 5-point star, rectangle, diamond, hexagon, random polygon, and confetti
|
||||||
|
- Custom sparkle shapes via parameterized `SparkleShapeSpec` factories
|
||||||
|
- Cross-platform Flutter support (mobile, web, desktop)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add the package:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
holo_shiny: ^0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:holo_shiny/holo_shiny.dart';
|
||||||
|
|
||||||
|
class Demo extends StatelessWidget {
|
||||||
|
const Demo({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Shiny(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Color(0xFF202A3A),
|
||||||
|
child: SizedBox(width: 280, height: 80),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Card Usage
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ShinyCard(
|
||||||
|
controller: controller,
|
||||||
|
background: Container(color: const Color(0xFF1B2D4B)),
|
||||||
|
foreground: const Center(child: Text('HOLO')),
|
||||||
|
sparkleShape: SparkleShapeSpec.eightPointStar,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Motion with Controller
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final ShinyController controller = ShinyController(useSensor: true);
|
||||||
|
|
||||||
|
Shiny(
|
||||||
|
controller: controller,
|
||||||
|
child: Container(color: const Color(0xFF1B2D4B)),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sparkle Shapes
|
||||||
|
|
||||||
|
Use built-in sparkle presets:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ShinyCard(
|
||||||
|
sparkleShape: SparkleShapeSpec.hexagon,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or create your own parameterized shapes:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
ShinyCard(
|
||||||
|
sparkleShape: SparkleShapeSpec.customStar(
|
||||||
|
points: 7,
|
||||||
|
innerRatio: 0.36,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Shiny(
|
||||||
|
sparkleShape: SparkleShapeSpec.customPolygon(
|
||||||
|
sides: 8,
|
||||||
|
aspectRatio: 1.2,
|
||||||
|
rotation: 0.2,
|
||||||
|
),
|
||||||
|
child: const SizedBox(width: 240, height: 120),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also provide a custom stream:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final StreamController<Offset> input = StreamController<Offset>.broadcast();
|
||||||
|
final ShinyController controller = ShinyController(tiltStream: input.stream);
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Summary
|
||||||
|
|
||||||
|
- `Shiny`: generic effect wrapper
|
||||||
|
- `ShinyCard`: shape + rotation + composition convenience widget
|
||||||
|
- `SparkleShapeSpec`: built-in and custom sparkle silhouette configuration
|
||||||
|
- `ShinyController`: optional source selection for tilt
|
||||||
|
- `SensorTiltController`: low-level sensor fusion stream utility
|
||||||
|
|
||||||
|
## Platform Notes
|
||||||
|
|
||||||
|
- Shader uses Flutter runtime effects and avoids derivative functions (`dFdx`, `dFdy`, `fwidth`) for web compatibility.
|
||||||
|
- If a sensor is unavailable, motion input simply emits no events.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
See the package example app in `example/` for:
|
||||||
|
|
||||||
|
- Basic usage
|
||||||
|
- Any-widget shiny wrapper usage
|
||||||
|
- Sensor-driven motion
|
||||||
|
- External stream override
|
||||||
|
- Custom card shape/background/foreground composition
|
||||||
|
- Built-in sparkle shape toggles in the demo UI
|
||||||
|
- User-defined sparkle shape presets using `SparkleShapeSpec`
|
||||||
|
|
||||||
|
## Publishing Notes
|
||||||
|
|
||||||
|
Replace placeholder package metadata URLs in `pubspec.yaml` before publishing.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.holo_shiny"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.holo_shiny"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="holo_shiny"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.holo_shiny
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "db50e20168db8fee486b9abf32fc912de3bc5b6a"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
- platform: android
|
||||||
|
create_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
base_revision: db50e20168db8fee486b9abf32fc912de3bc5b6a
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# holo_shiny_example
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.holo_shiny_example"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
|
applicationId = "com.example.holo_shiny_example"
|
||||||
|
// You can update the following values to match your application needs.
|
||||||
|
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||||
|
minSdk = flutter.minSdkVersion
|
||||||
|
targetSdk = flutter.targetSdkVersion
|
||||||
|
versionCode = flutter.versionCode
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
// TODO: Add your own signing config for the release build.
|
||||||
|
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<application
|
||||||
|
android:label="holo_shiny_example"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
|
the Android process has started. This theme is visible to the user
|
||||||
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
|
to determine the Window background behind the Flutter UI. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.example.holo_shiny_example
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
|
After Width: | Height: | Size: 544 B |
|
After Width: | Height: | Size: 442 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory =
|
||||||
|
rootProject.layout.buildDirectory
|
||||||
|
.dir("../../build")
|
||||||
|
.get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath =
|
||||||
|
run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
|
id("com.android.application") version "8.11.1" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
|
After Width: | Height: | Size: 300 KiB |
@@ -0,0 +1,418 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:holo_shiny/holo_shiny.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const ExampleApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExampleApp extends StatelessWidget {
|
||||||
|
const ExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF24A6A8)),
|
||||||
|
),
|
||||||
|
home: const ExampleHome(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExampleHome extends StatefulWidget {
|
||||||
|
const ExampleHome({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ExampleHome> createState() => _ExampleHomeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ExampleHomeState extends State<ExampleHome> {
|
||||||
|
late final ShinyController _sensorController;
|
||||||
|
late final StreamController<Offset> _externalTiltController;
|
||||||
|
late final ShinyController _overrideController;
|
||||||
|
|
||||||
|
double _prismatic = 0.8;
|
||||||
|
double _sparkle = 0.8;
|
||||||
|
double _specular = 0.8;
|
||||||
|
double _diffraction = 0.8;
|
||||||
|
HolographStyle _style = HolographStyle.crackedIce;
|
||||||
|
SparkleShapeSpec _sparkleShape = SparkleShapeSpec.eightPointStar;
|
||||||
|
|
||||||
|
static const Map<String, SparkleShapeSpec> _sparkleChoices =
|
||||||
|
<String, SparkleShapeSpec>{
|
||||||
|
'8-Point Star': SparkleShapeSpec.eightPointStar,
|
||||||
|
'5-Point Star': SparkleShapeSpec.fivePointStar,
|
||||||
|
'Rectangle': SparkleShapeSpec.rectangle,
|
||||||
|
'Diamond': SparkleShapeSpec.diamond,
|
||||||
|
'Hexagon': SparkleShapeSpec.hexagon,
|
||||||
|
'Random Polygon': SparkleShapeSpec.randomPolygon,
|
||||||
|
'Confetti': SparkleShapeSpec.confetti,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_sensorController = ShinyController(useSensor: true);
|
||||||
|
_externalTiltController = StreamController<Offset>.broadcast();
|
||||||
|
_overrideController =
|
||||||
|
ShinyController(tiltStream: _externalTiltController.stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_sensorController.dispose();
|
||||||
|
_overrideController.dispose();
|
||||||
|
_externalTiltController.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFF0D121A),
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('holo_shiny example'),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Shiny on Any Widget'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 320,
|
||||||
|
height: 120,
|
||||||
|
child: Shiny(
|
||||||
|
controller: _sensorController,
|
||||||
|
style: _style,
|
||||||
|
sparkleShape: _sparkleShape,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF18263E),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
child: const Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(Icons.auto_awesome, color: Colors.white),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'This can wrap any widget, including app chrome.',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text('ShinyCard (shape + rotation + shine)'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: ShinyCard(
|
||||||
|
controller: _sensorController,
|
||||||
|
style: _style,
|
||||||
|
sparkleShape: _sparkleShape,
|
||||||
|
background: const _DemoCardBackground(),
|
||||||
|
foreground: const _RareBadge(),
|
||||||
|
prismatic: _prismatic,
|
||||||
|
sparkle: _sparkle,
|
||||||
|
specular: _specular,
|
||||||
|
diffraction: _diffraction,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_StylePicker(
|
||||||
|
selectedStyle: _style,
|
||||||
|
onChanged: (HolographStyle style) {
|
||||||
|
setState(() {
|
||||||
|
_style = style;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_SparkleShapePicker(
|
||||||
|
selectedShape: _sparkleShape,
|
||||||
|
choices: _sparkleChoices,
|
||||||
|
onChanged: (SparkleShapeSpec shape) {
|
||||||
|
setState(() {
|
||||||
|
_sparkleShape = shape;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_LabeledSlider('Prismatic', _prismatic, (double value) {
|
||||||
|
setState(() {
|
||||||
|
_prismatic = value;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
_LabeledSlider('Sparkle', _sparkle, (double value) {
|
||||||
|
setState(() {
|
||||||
|
_sparkle = value;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
_LabeledSlider('Specular', _specular, (double value) {
|
||||||
|
setState(() {
|
||||||
|
_specular = value;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
_LabeledSlider('Diffraction', _diffraction, (double value) {
|
||||||
|
setState(() {
|
||||||
|
_diffraction = value;
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text('External Tilt Stream + Custom Shape (card)'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
_externalTiltController.add(const Offset(0.7, 0.0)),
|
||||||
|
child: const Text('Tilt Right'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () =>
|
||||||
|
_externalTiltController.add(const Offset(-0.7, 0.0)),
|
||||||
|
child: const Text('Tilt Left'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: ShinyCard(
|
||||||
|
controller: _overrideController,
|
||||||
|
style: _style,
|
||||||
|
sparkleShape: _sparkleShape,
|
||||||
|
shape: const StadiumBorder(),
|
||||||
|
background: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: <Color>[Color(0xFF1A2A44), Color(0xFF15263D)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
foreground: const Center(
|
||||||
|
child: Text(
|
||||||
|
'EXTERNAL STREAM',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: 1.2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LabeledSlider extends StatelessWidget {
|
||||||
|
const _LabeledSlider(this.label, this.value, this.onChanged);
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final double value;
|
||||||
|
final ValueChanged<double> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(label),
|
||||||
|
Slider(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _StylePicker extends StatelessWidget {
|
||||||
|
const _StylePicker({
|
||||||
|
required this.selectedStyle,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final HolographStyle selectedStyle;
|
||||||
|
final ValueChanged<HolographStyle> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Style'),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: HolographStyle.values.map((HolographStyle style) {
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(_styleLabel(style)),
|
||||||
|
selected: selectedStyle == style,
|
||||||
|
onSelected: (bool selected) {
|
||||||
|
if (!selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChanged(style);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _styleLabel(HolographStyle style) {
|
||||||
|
switch (style) {
|
||||||
|
case HolographStyle.holographicSilver:
|
||||||
|
return 'Holographic Silver';
|
||||||
|
case HolographStyle.crackedIce:
|
||||||
|
return 'Cracked Ice';
|
||||||
|
case HolographStyle.silverMosaic:
|
||||||
|
return 'Silver Mosaic';
|
||||||
|
case HolographStyle.superGoldVinyl:
|
||||||
|
return 'Super Gold Vinyl';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SparkleShapePicker extends StatelessWidget {
|
||||||
|
const _SparkleShapePicker({
|
||||||
|
required this.selectedShape,
|
||||||
|
required this.choices,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
final SparkleShapeSpec selectedShape;
|
||||||
|
final Map<String, SparkleShapeSpec> choices;
|
||||||
|
final ValueChanged<SparkleShapeSpec> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Sparkle Shape'),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children:
|
||||||
|
choices.entries.map((MapEntry<String, SparkleShapeSpec> entry) {
|
||||||
|
return ChoiceChip(
|
||||||
|
label: Text(entry.key),
|
||||||
|
selected: selectedShape == entry.value,
|
||||||
|
onSelected: (bool selected) {
|
||||||
|
if (!selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChanged(entry.value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DemoCardBackground extends StatelessWidget {
|
||||||
|
const _DemoCardBackground();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: <Widget>[
|
||||||
|
Image.asset(
|
||||||
|
'assets/pokemon.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
errorBuilder:
|
||||||
|
(BuildContext context, Object error, StackTrace? stackTrace) {
|
||||||
|
return Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: <Color>[Color(0xFF2D1B1B), Color(0xFF120D18)],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: const Align(
|
||||||
|
alignment: Alignment.bottomLeft,
|
||||||
|
child: Text(
|
||||||
|
'Demo Card',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[
|
||||||
|
Colors.black.withValues(alpha: 0.10),
|
||||||
|
Colors.black.withValues(alpha: 0.22),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RareBadge extends StatelessWidget {
|
||||||
|
const _RareBadge();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.all(12),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(99),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'RARE',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
flutter/ephemeral
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# Project-level configuration.
|
||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# The name of the executable created for the application. Change this to change
|
||||||
|
# the on-disk name of your application.
|
||||||
|
set(BINARY_NAME "holo_shiny_example")
|
||||||
|
# The unique GTK application identifier for this application. See:
|
||||||
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
|
set(APPLICATION_ID "com.example.holo_shiny_example")
|
||||||
|
|
||||||
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
|
# versions of CMake.
|
||||||
|
cmake_policy(SET CMP0063 NEW)
|
||||||
|
|
||||||
|
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||||
|
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||||
|
|
||||||
|
# Root filesystem for cross-building.
|
||||||
|
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||||
|
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define build configuration options.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||||
|
STRING "Flutter build mode" FORCE)
|
||||||
|
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||||
|
"Debug" "Profile" "Release")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Compilation settings that should be applied to most targets.
|
||||||
|
#
|
||||||
|
# Be cautious about adding new options here, as plugins use this function by
|
||||||
|
# default. In most cases, you should add new options to specific targets instead
|
||||||
|
# of modifying this function.
|
||||||
|
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||||
|
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||||
|
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||||
|
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||||
|
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Flutter library and tool build rules.
|
||||||
|
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||||
|
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||||
|
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
|
||||||
|
# Application build; see runner/CMakeLists.txt.
|
||||||
|
add_subdirectory("runner")
|
||||||
|
|
||||||
|
# Run the Flutter tool portions of the build. This must not be removed.
|
||||||
|
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||||
|
|
||||||
|
# Only the install-generated bundle's copy of the executable will launch
|
||||||
|
# correctly, since the resources must in the right relative locations. To avoid
|
||||||
|
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||||
|
# the default top-level location.
|
||||||
|
set_target_properties(${BINARY_NAME}
|
||||||
|
PROPERTIES
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Generated plugin build rules, which manage building the plugins and adding
|
||||||
|
# them to the application.
|
||||||
|
include(flutter/generated_plugins.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
# === Installation ===
|
||||||
|
# By default, "installing" just makes a relocatable bundle in the build
|
||||||
|
# directory.
|
||||||
|
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||||
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||||
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Start with a clean build bundle directory every time.
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
|
||||||
|
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||||
|
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||||
|
|
||||||
|
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||||
|
install(FILES "${bundled_library}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endforeach(bundled_library)
|
||||||
|
|
||||||
|
# Copy the native assets provided by the build.dart from all packages.
|
||||||
|
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||||
|
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||||
|
# from a previous install.
|
||||||
|
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||||
|
install(CODE "
|
||||||
|
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||||
|
" COMPONENT Runtime)
|
||||||
|
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||||
|
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||||
|
|
||||||
|
# Install the AOT library on non-Debug builds only.
|
||||||
|
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||||
|
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||||
|
COMPONENT Runtime)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# This file controls Flutter-level build steps. It should not be edited.
|
||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
|
||||||
|
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||||
|
|
||||||
|
# Configuration provided via flutter tool.
|
||||||
|
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||||
|
|
||||||
|
# TODO: Move the rest of this into files in ephemeral. See
|
||||||
|
# https://github.com/flutter/flutter/issues/57146.
|
||||||
|
|
||||||
|
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||||
|
# which isn't available in 3.10.
|
||||||
|
function(list_prepend LIST_NAME PREFIX)
|
||||||
|
set(NEW_LIST "")
|
||||||
|
foreach(element ${${LIST_NAME}})
|
||||||
|
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||||
|
endforeach(element)
|
||||||
|
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# === Flutter Library ===
|
||||||
|
# System-level dependencies.
|
||||||
|
find_package(PkgConfig REQUIRED)
|
||||||
|
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||||
|
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||||
|
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||||
|
|
||||||
|
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||||
|
|
||||||
|
# Published to parent scope for install step.
|
||||||
|
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||||
|
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||||
|
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||||
|
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||||
|
"fl_basic_message_channel.h"
|
||||||
|
"fl_binary_codec.h"
|
||||||
|
"fl_binary_messenger.h"
|
||||||
|
"fl_dart_project.h"
|
||||||
|
"fl_engine.h"
|
||||||
|
"fl_json_message_codec.h"
|
||||||
|
"fl_json_method_codec.h"
|
||||||
|
"fl_message_codec.h"
|
||||||
|
"fl_method_call.h"
|
||||||
|
"fl_method_channel.h"
|
||||||
|
"fl_method_codec.h"
|
||||||
|
"fl_method_response.h"
|
||||||
|
"fl_plugin_registrar.h"
|
||||||
|
"fl_plugin_registry.h"
|
||||||
|
"fl_standard_message_codec.h"
|
||||||
|
"fl_standard_method_codec.h"
|
||||||
|
"fl_string_codec.h"
|
||||||
|
"fl_value.h"
|
||||||
|
"fl_view.h"
|
||||||
|
"flutter_linux.h"
|
||||||
|
)
|
||||||
|
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||||
|
add_library(flutter INTERFACE)
|
||||||
|
target_include_directories(flutter INTERFACE
|
||||||
|
"${EPHEMERAL_DIR}"
|
||||||
|
)
|
||||||
|
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||||
|
target_link_libraries(flutter INTERFACE
|
||||||
|
PkgConfig::GTK
|
||||||
|
PkgConfig::GLIB
|
||||||
|
PkgConfig::GIO
|
||||||
|
)
|
||||||
|
add_dependencies(flutter flutter_assemble)
|
||||||
|
|
||||||
|
# === Flutter tool backend ===
|
||||||
|
# _phony_ is a non-existent file to force this command to run every time,
|
||||||
|
# since currently there's no way to get a full input/output list from the
|
||||||
|
# flutter tool.
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env
|
||||||
|
${FLUTTER_TOOL_ENVIRONMENT}
|
||||||
|
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||||
|
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(flutter_assemble DEPENDS
|
||||||
|
"${FLUTTER_LIBRARY}"
|
||||||
|
${FLUTTER_LIBRARY_HEADERS}
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Generated file. Do not edit.
|
||||||
|
//
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
#define GENERATED_PLUGIN_REGISTRANT_
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
|
||||||
|
// Registers Flutter plugins.
|
||||||
|
void fl_register_plugins(FlPluginRegistry* registry);
|
||||||
|
|
||||||
|
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#
|
||||||
|
# Generated file, do not edit.
|
||||||
|
#
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
)
|
||||||
|
|
||||||
|
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||||
|
|
||||||
|
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||||
|
endforeach(plugin)
|
||||||
|
|
||||||
|
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||||
|
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||||
|
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||||
|
endforeach(ffi_plugin)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
project(runner LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Define the application target. To change its name, change BINARY_NAME in the
|
||||||
|
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||||
|
# work.
|
||||||
|
#
|
||||||
|
# Any new source files that you add to the application should be added here.
|
||||||
|
add_executable(${BINARY_NAME}
|
||||||
|
"main.cc"
|
||||||
|
"my_application.cc"
|
||||||
|
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply the standard set of build settings. This can be removed for applications
|
||||||
|
# that need different build settings.
|
||||||
|
apply_standard_settings(${BINARY_NAME})
|
||||||
|
|
||||||
|
# Add preprocessor definitions for the application ID.
|
||||||
|
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||||
|
|
||||||
|
# Add dependency libraries. Add any application-specific dependencies here.
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||||
|
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||||
|
|
||||||
|
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
g_autoptr(MyApplication) app = my_application_new();
|
||||||
|
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
#include "my_application.h"
|
||||||
|
|
||||||
|
#include <flutter_linux/flutter_linux.h>
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "flutter/generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
struct _MyApplication {
|
||||||
|
GtkApplication parent_instance;
|
||||||
|
char** dart_entrypoint_arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||||
|
|
||||||
|
// Called when first Flutter frame received.
|
||||||
|
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||||
|
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::activate.
|
||||||
|
static void my_application_activate(GApplication* application) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
GtkWindow* window =
|
||||||
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
|
// Use a header bar when running in GNOME as this is the common style used
|
||||||
|
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||||
|
// desktop).
|
||||||
|
// If running on X and not using GNOME then just use a traditional title bar
|
||||||
|
// in case the window manager does more exotic layout, e.g. tiling.
|
||||||
|
// If running on Wayland assume the header bar will work (may need changing
|
||||||
|
// if future cases occur).
|
||||||
|
gboolean use_header_bar = TRUE;
|
||||||
|
#ifdef GDK_WINDOWING_X11
|
||||||
|
GdkScreen* screen = gtk_window_get_screen(window);
|
||||||
|
if (GDK_IS_X11_SCREEN(screen)) {
|
||||||
|
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||||
|
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||||
|
use_header_bar = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (use_header_bar) {
|
||||||
|
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||||
|
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||||
|
gtk_header_bar_set_title(header_bar, "holo_shiny_example");
|
||||||
|
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||||
|
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||||
|
} else {
|
||||||
|
gtk_window_set_title(window, "holo_shiny_example");
|
||||||
|
}
|
||||||
|
|
||||||
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
|
||||||
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
|
fl_dart_project_set_dart_entrypoint_arguments(
|
||||||
|
project, self->dart_entrypoint_arguments);
|
||||||
|
|
||||||
|
FlView* view = fl_view_new(project);
|
||||||
|
GdkRGBA background_color;
|
||||||
|
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||||
|
// for transparent.
|
||||||
|
gdk_rgba_parse(&background_color, "#000000");
|
||||||
|
fl_view_set_background_color(view, &background_color);
|
||||||
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
|
// Show the window when Flutter renders.
|
||||||
|
// Requires the view to be realized so we can start rendering.
|
||||||
|
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||||
|
self);
|
||||||
|
gtk_widget_realize(GTK_WIDGET(view));
|
||||||
|
|
||||||
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::local_command_line.
|
||||||
|
static gboolean my_application_local_command_line(GApplication* application,
|
||||||
|
gchar*** arguments,
|
||||||
|
int* exit_status) {
|
||||||
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
// Strip out the first argument as it is the binary name.
|
||||||
|
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||||
|
|
||||||
|
g_autoptr(GError) error = nullptr;
|
||||||
|
if (!g_application_register(application, nullptr, &error)) {
|
||||||
|
g_warning("Failed to register: %s", error->message);
|
||||||
|
*exit_status = 1;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_application_activate(application);
|
||||||
|
*exit_status = 0;
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::startup.
|
||||||
|
static void my_application_startup(GApplication* application) {
|
||||||
|
// MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
|
// Perform any actions required at application startup.
|
||||||
|
|
||||||
|
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GApplication::shutdown.
|
||||||
|
static void my_application_shutdown(GApplication* application) {
|
||||||
|
// MyApplication* self = MY_APPLICATION(object);
|
||||||
|
|
||||||
|
// Perform any actions required at application shutdown.
|
||||||
|
|
||||||
|
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements GObject::dispose.
|
||||||
|
static void my_application_dispose(GObject* object) {
|
||||||
|
MyApplication* self = MY_APPLICATION(object);
|
||||||
|
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||||
|
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_class_init(MyApplicationClass* klass) {
|
||||||
|
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||||
|
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||||
|
my_application_local_command_line;
|
||||||
|
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||||
|
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||||
|
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void my_application_init(MyApplication* self) {}
|
||||||
|
|
||||||
|
MyApplication* my_application_new() {
|
||||||
|
// Set the program name to the application ID, which helps various systems
|
||||||
|
// like GTK and desktop environments map this running application to its
|
||||||
|
// corresponding .desktop file. This ensures better integration by allowing
|
||||||
|
// the application to be recognized beyond its binary name.
|
||||||
|
g_set_prgname(APPLICATION_ID);
|
||||||
|
|
||||||
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
|
"application-id", APPLICATION_ID, "flags",
|
||||||
|
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||||
|
#define FLUTTER_MY_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||||
|
my_application,
|
||||||
|
MY,
|
||||||
|
APPLICATION,
|
||||||
|
GtkApplication)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* my_application_new:
|
||||||
|
*
|
||||||
|
* Creates a new Flutter-based application.
|
||||||
|
*
|
||||||
|
* Returns: a new #MyApplication.
|
||||||
|
*/
|
||||||
|
MyApplication* my_application_new();
|
||||||
|
|
||||||
|
#endif // FLUTTER_MY_APPLICATION_H_
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: holo_shiny_example
|
||||||
|
description: Example app for holo_shiny.
|
||||||
|
publish_to: none
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.6.0
|
||||||
|
flutter: ">=3.22.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
holo_shiny:
|
||||||
|
path: ..
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
assets:
|
||||||
|
- assets/
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export 'src/sensor_tilt_controller.dart';
|
||||||
|
export 'src/shiny_controller.dart';
|
||||||
|
export 'src/shiny_widget.dart';
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const MainApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MainApp extends StatelessWidget {
|
||||||
|
const MainApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Text('Hello World!'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:sensors_plus/sensors_plus.dart';
|
||||||
|
|
||||||
|
/// Produces a normalized tilt stream from gyroscope and accelerometer data.
|
||||||
|
class SensorTiltController {
|
||||||
|
/// Creates and starts the sensor controller.
|
||||||
|
SensorTiltController({
|
||||||
|
this.alpha = 0.98,
|
||||||
|
this.maxTiltRadians = 0.5,
|
||||||
|
}) : _invAlpha = 1.0 - alpha {
|
||||||
|
_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complementary filter blending coefficient.
|
||||||
|
final double alpha;
|
||||||
|
|
||||||
|
/// Cached inverse alpha to avoid repetitive subtractions.
|
||||||
|
final double _invAlpha;
|
||||||
|
|
||||||
|
/// Physical angle that maps to normalized magnitude `1.0`.
|
||||||
|
final double maxTiltRadians;
|
||||||
|
|
||||||
|
final StreamController<Offset> _controller =
|
||||||
|
StreamController<Offset>.broadcast();
|
||||||
|
|
||||||
|
// High-performance timing for tight physics loops
|
||||||
|
final Stopwatch _stopwatch = Stopwatch();
|
||||||
|
|
||||||
|
StreamSubscription<GyroscopeEvent>? _gyroSub;
|
||||||
|
StreamSubscription<AccelerometerEvent>? _accelSub;
|
||||||
|
|
||||||
|
double _roll = 0.0;
|
||||||
|
double _pitch = 0.0;
|
||||||
|
double _accelRoll = 0.0;
|
||||||
|
double _accelPitch = 0.0;
|
||||||
|
int? _lastMicroseconds;
|
||||||
|
|
||||||
|
/// The normalized tilt stream.
|
||||||
|
Stream<Offset> get stream => _controller.stream;
|
||||||
|
|
||||||
|
void _start() {
|
||||||
|
if (!_supportsSensorStreams) return;
|
||||||
|
|
||||||
|
_stopwatch.start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
_gyroSub = gyroscopeEventStream().listen(
|
||||||
|
_onGyro,
|
||||||
|
onError: (_) {},
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
} on MissingPluginException {
|
||||||
|
_gyroSub = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
_accelSub = accelerometerEventStream().listen(
|
||||||
|
_onAccel,
|
||||||
|
onError: (_) {},
|
||||||
|
cancelOnError: false,
|
||||||
|
);
|
||||||
|
} on MissingPluginException {
|
||||||
|
_accelSub = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _supportsSensorStreams {
|
||||||
|
if (kIsWeb) return true;
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return true;
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onGyro(GyroscopeEvent event) {
|
||||||
|
final int nowMicros = _stopwatch.elapsedMicroseconds;
|
||||||
|
final int? last = _lastMicroseconds;
|
||||||
|
_lastMicroseconds = nowMicros;
|
||||||
|
|
||||||
|
if (last == null) return;
|
||||||
|
|
||||||
|
final double dt = (nowMicros - last) / 1000000.0;
|
||||||
|
if (dt <= 0.0) return;
|
||||||
|
|
||||||
|
final double gyroRoll = _roll + (event.y * dt);
|
||||||
|
final double gyroPitch = _pitch + (event.x * dt);
|
||||||
|
|
||||||
|
_roll = (alpha * gyroRoll) + (_invAlpha * _accelRoll);
|
||||||
|
_pitch = (alpha * gyroPitch) + (_invAlpha * _accelPitch);
|
||||||
|
|
||||||
|
_controller.add(_normalizedOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAccel(AccelerometerEvent event) {
|
||||||
|
const double g = 9.81;
|
||||||
|
_accelRoll = (event.x / g).clamp(-1.0, 1.0);
|
||||||
|
_accelPitch = (event.y / g).clamp(-1.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _normalizedOffset() {
|
||||||
|
final double dx = (_roll / maxTiltRadians).clamp(-1.0, 1.0);
|
||||||
|
final double dy = (_pitch / maxTiltRadians).clamp(-1.0, 1.0);
|
||||||
|
return Offset(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops sensor streams and releases resources.
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_stopwatch.stop();
|
||||||
|
await _gyroSub?.cancel();
|
||||||
|
await _accelSub?.cancel();
|
||||||
|
await _controller.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'sensor_tilt_controller.dart';
|
||||||
|
|
||||||
|
/// Optional controller for providing tilt input to [Shiny] widgets.
|
||||||
|
///
|
||||||
|
/// By default, no stream is emitted. Set [useSensor] to true to use built-in
|
||||||
|
/// motion sensors, or provide [tiltStream] to fully control tilt externally.
|
||||||
|
class ShinyController {
|
||||||
|
/// Creates a [ShinyController].
|
||||||
|
///
|
||||||
|
/// If [tiltStream] is provided, it overrides the built-in sensor stream.
|
||||||
|
ShinyController({
|
||||||
|
this.useSensor = false,
|
||||||
|
this.tiltStream,
|
||||||
|
}) {
|
||||||
|
if (tiltStream == null && useSensor) {
|
||||||
|
_sensorController = SensorTiltController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables internal sensor-based tilt when [tiltStream] is null.
|
||||||
|
final bool useSensor;
|
||||||
|
|
||||||
|
/// Optional external tilt stream in normalized `[-1.0, 1.0]` space.
|
||||||
|
final Stream<Offset>? tiltStream;
|
||||||
|
|
||||||
|
SensorTiltController? _sensorController;
|
||||||
|
|
||||||
|
/// The effective tilt stream consumed by [Shiny].
|
||||||
|
Stream<Offset> get stream {
|
||||||
|
if (tiltStream != null) {
|
||||||
|
return tiltStream!;
|
||||||
|
}
|
||||||
|
if (_sensorController != null) {
|
||||||
|
return _sensorController!.stream;
|
||||||
|
}
|
||||||
|
return const Stream<Offset>.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disposes internally-owned resources.
|
||||||
|
Future<void> dispose() async {
|
||||||
|
await _sensorController?.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|
||||||
|
import 'shiny_controller.dart';
|
||||||
|
|
||||||
|
enum HolographStyle {
|
||||||
|
holographicSilver,
|
||||||
|
crackedIce,
|
||||||
|
silverMosaic,
|
||||||
|
superGoldVinyl,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SparkleShapeKind {
|
||||||
|
star,
|
||||||
|
rectangle,
|
||||||
|
polygon,
|
||||||
|
randomPolygon,
|
||||||
|
confetti,
|
||||||
|
}
|
||||||
|
|
||||||
|
class SparkleShapeSpec {
|
||||||
|
const SparkleShapeSpec._({
|
||||||
|
required this.kind,
|
||||||
|
required this.primary,
|
||||||
|
required this.secondary,
|
||||||
|
required this.tertiary,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const SparkleShapeSpec eightPointStar = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.star,
|
||||||
|
primary: 8.0,
|
||||||
|
secondary: 0.42,
|
||||||
|
tertiary: 0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SparkleShapeSpec fivePointStar = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.star,
|
||||||
|
primary: 5.0,
|
||||||
|
secondary: 0.42,
|
||||||
|
tertiary: 0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SparkleShapeSpec rectangle = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.rectangle,
|
||||||
|
primary: 0.24,
|
||||||
|
secondary: 0.055,
|
||||||
|
tertiary: 1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SparkleShapeSpec diamond = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.polygon,
|
||||||
|
primary: 4.0,
|
||||||
|
secondary: 1.0,
|
||||||
|
tertiary: 0.78539816339,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SparkleShapeSpec hexagon = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.polygon,
|
||||||
|
primary: 6.0,
|
||||||
|
secondary: 1.0,
|
||||||
|
tertiary: 0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SparkleShapeSpec randomPolygon = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.randomPolygon,
|
||||||
|
primary: 0.0,
|
||||||
|
secondary: 0.0,
|
||||||
|
tertiary: 0.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const SparkleShapeSpec confetti = SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.confetti,
|
||||||
|
primary: 0.34,
|
||||||
|
secondary: 0.045,
|
||||||
|
tertiary: 1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
factory SparkleShapeSpec.customStar(
|
||||||
|
{int points = 8, double innerRatio = 0.42}) {
|
||||||
|
return SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.star,
|
||||||
|
primary: points.toDouble().clamp(4.0, 12.0),
|
||||||
|
secondary: innerRatio.clamp(0.15, 0.85),
|
||||||
|
tertiary: 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SparkleShapeSpec.customPolygon(
|
||||||
|
{int sides = 6, double aspectRatio = 1.0, double rotation = 0.0}) {
|
||||||
|
return SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.polygon,
|
||||||
|
primary: sides.toDouble().clamp(3.0, 10.0),
|
||||||
|
secondary: aspectRatio.clamp(0.4, 2.0),
|
||||||
|
tertiary: rotation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SparkleShapeSpec.customRectangle(
|
||||||
|
{double halfWidth = 0.24,
|
||||||
|
double halfHeight = 0.055,
|
||||||
|
double rotationJitter = 1.0}) {
|
||||||
|
return SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.rectangle,
|
||||||
|
primary: halfWidth.clamp(0.05, 0.45),
|
||||||
|
secondary: halfHeight.clamp(0.02, 0.30),
|
||||||
|
tertiary: rotationJitter.clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory SparkleShapeSpec.customConfetti(
|
||||||
|
{double length = 0.34,
|
||||||
|
double thickness = 0.045,
|
||||||
|
double rotationJitter = 1.0}) {
|
||||||
|
return SparkleShapeSpec._(
|
||||||
|
kind: SparkleShapeKind.confetti,
|
||||||
|
primary: length.clamp(0.08, 0.48),
|
||||||
|
secondary: thickness.clamp(0.01, 0.14),
|
||||||
|
tertiary: rotationJitter.clamp(0.0, 1.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final SparkleShapeKind kind;
|
||||||
|
final double primary;
|
||||||
|
final double secondary;
|
||||||
|
final double tertiary;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Shiny extends StatefulWidget {
|
||||||
|
const Shiny({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.controller,
|
||||||
|
this.tilt,
|
||||||
|
this.prismatic = 0.8,
|
||||||
|
this.sparkle = 0.8,
|
||||||
|
this.specular = 0.8,
|
||||||
|
this.diffraction = 0.8,
|
||||||
|
this.sparkleShape = SparkleShapeSpec.eightPointStar,
|
||||||
|
this.style = HolographStyle.crackedIce,
|
||||||
|
this.blendMode = BlendMode.screen,
|
||||||
|
this.enableShader = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final ShinyController? controller;
|
||||||
|
final Offset? tilt;
|
||||||
|
final double prismatic;
|
||||||
|
final double sparkle;
|
||||||
|
final double specular;
|
||||||
|
final double diffraction;
|
||||||
|
final SparkleShapeSpec sparkleShape;
|
||||||
|
final HolographStyle style;
|
||||||
|
final BlendMode blendMode;
|
||||||
|
final bool enableShader;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Shiny> createState() => _ShinyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShinyState extends State<Shiny> with TickerProviderStateMixin {
|
||||||
|
static Future<ui.FragmentProgram>? _programFuture;
|
||||||
|
|
||||||
|
ui.FragmentShader? _shader;
|
||||||
|
StreamSubscription<Offset>? _tiltSub;
|
||||||
|
late final Ticker _ticker;
|
||||||
|
|
||||||
|
double _time = 0.0;
|
||||||
|
Offset _tilt = Offset.zero;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_ticker = createTicker((Duration elapsed) {
|
||||||
|
if (!mounted || _shader == null) return;
|
||||||
|
setState(() {
|
||||||
|
_time = elapsed.inMicroseconds / 1000000.0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_attachController();
|
||||||
|
if (widget.enableShader) {
|
||||||
|
_loadShader();
|
||||||
|
_ticker.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant Shiny oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.controller != widget.controller ||
|
||||||
|
oldWidget.tilt != widget.tilt) {
|
||||||
|
_tiltSub?.cancel();
|
||||||
|
_attachController();
|
||||||
|
}
|
||||||
|
if (oldWidget.enableShader != widget.enableShader) {
|
||||||
|
if (widget.enableShader) {
|
||||||
|
if (_shader == null) _loadShader();
|
||||||
|
_ticker.start();
|
||||||
|
} else {
|
||||||
|
_ticker.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadShader() async {
|
||||||
|
try {
|
||||||
|
_programFuture ??= _loadProgram();
|
||||||
|
final ui.FragmentProgram program = await _programFuture!;
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_shader = program.fragmentShader();
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Keep rendering without shader when runtime effects are unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ui.FragmentProgram> _loadProgram() async {
|
||||||
|
try {
|
||||||
|
return await ui.FragmentProgram.fromAsset('shaders/shiny_card.frag');
|
||||||
|
} catch (_) {
|
||||||
|
return await ui.FragmentProgram.fromAsset(
|
||||||
|
'packages/holo_shiny/shaders/shiny_card.frag');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _attachController() {
|
||||||
|
if (widget.tilt != null) return;
|
||||||
|
final ShinyController? controller = widget.controller;
|
||||||
|
if (controller == null) return;
|
||||||
|
|
||||||
|
_tiltSub = controller.stream.listen((Offset value) {
|
||||||
|
if (!mounted || widget.tilt != null) return;
|
||||||
|
final clamped = _clampOffset(value);
|
||||||
|
if (_tilt != clamped) {
|
||||||
|
setState(() {
|
||||||
|
_tilt = clamped;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ticker.dispose();
|
||||||
|
_tiltSub?.cancel();
|
||||||
|
_shader?.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Shader _configureShader(Rect bounds) {
|
||||||
|
final Offset effectiveTilt = _clampOffset(widget.tilt ?? _tilt);
|
||||||
|
final ui.FragmentShader shader = _shader!;
|
||||||
|
shader
|
||||||
|
..setFloat(0, bounds.width)
|
||||||
|
..setFloat(1, bounds.height)
|
||||||
|
..setFloat(2, effectiveTilt.dx)
|
||||||
|
..setFloat(3, effectiveTilt.dy)
|
||||||
|
..setFloat(4, _time)
|
||||||
|
..setFloat(5, widget.prismatic)
|
||||||
|
..setFloat(6, widget.sparkle)
|
||||||
|
..setFloat(7, widget.specular)
|
||||||
|
..setFloat(8, widget.diffraction)
|
||||||
|
..setFloat(9, widget.style.index.toDouble())
|
||||||
|
..setFloat(10, widget.sparkleShape.kind.index.toDouble())
|
||||||
|
..setFloat(11, widget.sparkleShape.primary)
|
||||||
|
..setFloat(12, widget.sparkleShape.secondary)
|
||||||
|
..setFloat(13, widget.sparkleShape.tertiary);
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (!widget.enableShader || _shader == null) return widget.child;
|
||||||
|
|
||||||
|
return ShaderMask(
|
||||||
|
blendMode: widget.blendMode,
|
||||||
|
shaderCallback: _configureShader,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _clampOffset(Offset value) {
|
||||||
|
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShinyCard extends StatefulWidget {
|
||||||
|
const ShinyCard({
|
||||||
|
super.key,
|
||||||
|
this.background,
|
||||||
|
this.foreground,
|
||||||
|
this.shape = const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16))),
|
||||||
|
this.width = 300,
|
||||||
|
this.height = 420,
|
||||||
|
this.controller,
|
||||||
|
this.prismatic = 0.8,
|
||||||
|
this.sparkle = 0.8,
|
||||||
|
this.specular = 0.8,
|
||||||
|
this.diffraction = 0.8,
|
||||||
|
this.sparkleShape = SparkleShapeSpec.eightPointStar,
|
||||||
|
this.style = HolographStyle.crackedIce,
|
||||||
|
this.enableShader = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget? background;
|
||||||
|
final Widget? foreground;
|
||||||
|
final ShapeBorder shape;
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final ShinyController? controller;
|
||||||
|
final double prismatic;
|
||||||
|
final double sparkle;
|
||||||
|
final double specular;
|
||||||
|
final double diffraction;
|
||||||
|
final SparkleShapeSpec sparkleShape;
|
||||||
|
final HolographStyle style;
|
||||||
|
final bool enableShader;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShinyCard> createState() => _ShinyCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShinyCardState extends State<ShinyCard>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
static const ValueKey<String> transformKey =
|
||||||
|
ValueKey<String>('holo_shiny.card.transform');
|
||||||
|
|
||||||
|
StreamSubscription<Offset>? _tiltSub;
|
||||||
|
late final AnimationController _spring;
|
||||||
|
|
||||||
|
// State segregation: prevents total widget rebuilds during drag events
|
||||||
|
late final ValueNotifier<Offset> _tiltNotifier = ValueNotifier(Offset.zero);
|
||||||
|
late final StreamController<Offset> _internalTiltStream =
|
||||||
|
StreamController<Offset>.broadcast();
|
||||||
|
late final ShinyController _internalShinyController =
|
||||||
|
ShinyController(tiltStream: _internalTiltStream.stream);
|
||||||
|
|
||||||
|
Offset _sensorTilt = Offset.zero;
|
||||||
|
Offset _dragTilt = Offset.zero;
|
||||||
|
Offset _dragTiltAtRelease = Offset.zero;
|
||||||
|
bool _isDragging = false;
|
||||||
|
bool _isReturning = false;
|
||||||
|
|
||||||
|
void _updateTilt() {
|
||||||
|
final tilt = (_isDragging || _isReturning) ? _dragTilt : _sensorTilt;
|
||||||
|
if (_tiltNotifier.value != tilt) {
|
||||||
|
_tiltNotifier.value = tilt;
|
||||||
|
if (!_internalTiltStream.isClosed) {
|
||||||
|
_internalTiltStream.add(tilt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_spring = AnimationController(
|
||||||
|
vsync: this, duration: const Duration(milliseconds: 850))
|
||||||
|
..addListener(() {
|
||||||
|
if (!mounted || !_isReturning) return;
|
||||||
|
final double t = Curves.elasticOut.transform(_spring.value);
|
||||||
|
_dragTilt =
|
||||||
|
Offset.lerp(_dragTiltAtRelease, Offset.zero, t) ?? Offset.zero;
|
||||||
|
_updateTilt();
|
||||||
|
})
|
||||||
|
..addStatusListener((AnimationStatus status) {
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
_isReturning = false;
|
||||||
|
_updateTilt();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_attachController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant ShinyCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.controller != widget.controller) {
|
||||||
|
_tiltSub?.cancel();
|
||||||
|
_attachController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _attachController() {
|
||||||
|
final ShinyController? controller = widget.controller;
|
||||||
|
if (controller == null) return;
|
||||||
|
|
||||||
|
_tiltSub = controller.stream.listen((Offset value) {
|
||||||
|
if (!mounted || _isDragging || _isReturning) return;
|
||||||
|
_sensorTilt = _clampOffset(value);
|
||||||
|
_updateTilt();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPanUpdate(DragUpdateDetails details) {
|
||||||
|
_isDragging = true;
|
||||||
|
_isReturning = false;
|
||||||
|
_spring.stop();
|
||||||
|
|
||||||
|
final double dx = (details.delta.dx / widget.width) * 2.0;
|
||||||
|
final double dy = (details.delta.dy / widget.height) * 2.0;
|
||||||
|
|
||||||
|
_dragTilt = _clampOffset(_dragTilt + Offset(dx, dy));
|
||||||
|
_updateTilt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPanEnd(DragEndDetails details) {
|
||||||
|
_isDragging = false;
|
||||||
|
_isReturning = true;
|
||||||
|
_dragTiltAtRelease = _dragTilt;
|
||||||
|
_spring
|
||||||
|
..reset()
|
||||||
|
..forward();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tiltSub?.cancel();
|
||||||
|
_spring.dispose();
|
||||||
|
_tiltNotifier.dispose();
|
||||||
|
_internalTiltStream.close();
|
||||||
|
_internalShinyController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Widget base = widget.background ??
|
||||||
|
const ColoredBox(color: Color(0xFF212121), child: SizedBox.expand());
|
||||||
|
|
||||||
|
// Isolate the static tree structure so it never rebuilds when tilted
|
||||||
|
final Widget staticChild = ClipPath(
|
||||||
|
clipper: ShapeBorderClipper(shape: widget.shape),
|
||||||
|
child: SizedBox(
|
||||||
|
width: widget.width,
|
||||||
|
height: widget.height,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: <Widget>[
|
||||||
|
Shiny(
|
||||||
|
enableShader: widget.enableShader,
|
||||||
|
controller: _internalShinyController,
|
||||||
|
tilt: null, // Managed natively via the internal stream controller
|
||||||
|
prismatic: widget.prismatic,
|
||||||
|
sparkle: widget.sparkle,
|
||||||
|
specular: widget.specular,
|
||||||
|
diffraction: widget.diffraction,
|
||||||
|
sparkleShape: widget.sparkleShape,
|
||||||
|
style: widget.style,
|
||||||
|
child: base,
|
||||||
|
),
|
||||||
|
if (widget.foreground != null) widget.foreground!,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onPanUpdate: _onPanUpdate,
|
||||||
|
onPanEnd: _onPanEnd,
|
||||||
|
child: ValueListenableBuilder<Offset>(
|
||||||
|
valueListenable: _tiltNotifier,
|
||||||
|
builder: (context, tilt, child) {
|
||||||
|
final Matrix4 transform = Matrix4.identity()
|
||||||
|
..setEntry(3, 2, 0.001)
|
||||||
|
..rotateX(-tilt.dy * 0.3)
|
||||||
|
..rotateY(tilt.dx * 0.3);
|
||||||
|
|
||||||
|
return Transform(
|
||||||
|
key: transformKey,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
transform: transform,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: staticChild,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Offset _clampOffset(Offset value) {
|
||||||
|
return Offset(value.dx.clamp(-1.0, 1.0), value.dy.clamp(-1.0, 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: holo_shiny
|
||||||
|
description: Holographic shiny card widget powered by GLSL fragment shaders.
|
||||||
|
version: 0.1.0
|
||||||
|
publish_to: none
|
||||||
|
repository: https://example.com/your-org/holo_shiny
|
||||||
|
homepage: https://example.com/your-org/holo_shiny
|
||||||
|
issue_tracker: https://example.com/your-org/holo_shiny/issues
|
||||||
|
documentation: https://example.com/your-org/holo_shiny/docs
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.6.0
|
||||||
|
flutter: ">=3.22.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
sensors_plus: ^7.0.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
shaders:
|
||||||
|
- shaders/shiny_card.frag
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
#version 460 core
|
||||||
|
#include <flutter/runtime_effect.glsl>
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
uniform vec2 uSize;
|
||||||
|
uniform vec2 uTilt;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uPrismatic;
|
||||||
|
uniform float uSparkle;
|
||||||
|
uniform float uSpecular;
|
||||||
|
uniform float uDiffraction;
|
||||||
|
uniform float uStyle;
|
||||||
|
uniform float uSparkleShapeKind;
|
||||||
|
uniform float uSparklePrimary;
|
||||||
|
uniform float uSparkleSecondary;
|
||||||
|
uniform float uSparkleTertiary;
|
||||||
|
|
||||||
|
#define TWO_PI 6.28318530718
|
||||||
|
|
||||||
|
// Precomputed rotation matrices to avoid expensive runtime sin/cos calculations
|
||||||
|
#define ROT_0_78 mat2( 0.71091, 0.70328, -0.70328, 0.71091)
|
||||||
|
#define ROT_M0_5 mat2( 0.87758, -0.47943, 0.47943, 0.87758)
|
||||||
|
#define ROT_1_2 mat2( 0.36236, 0.93204, -0.93204, 0.36236)
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||||
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float noise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
vec2 u = f * f * (3.0 - 2.0 * f);
|
||||||
|
|
||||||
|
float a = hash(i);
|
||||||
|
float b = hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = hash(i + vec2(1.0, 1.0));
|
||||||
|
|
||||||
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsv2rgb(float h, float s, float v) {
|
||||||
|
vec3 k = vec3(1.0, 2.0 / 3.0, 1.0 / 3.0);
|
||||||
|
vec3 p = abs(fract(vec3(h) + k) * 6.0 - 3.0);
|
||||||
|
return v * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), s);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 rainbow(float phase, float saturation, float value) {
|
||||||
|
return hsv2rgb(fract(phase), saturation, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdBox(vec2 p, vec2 b) {
|
||||||
|
vec2 d = abs(p) - b;
|
||||||
|
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sdRegularPolygon(vec2 p, float sides, float radius) {
|
||||||
|
float angle = atan(p.y, p.x);
|
||||||
|
float sector = TWO_PI / sides;
|
||||||
|
return cos(floor(0.5 + angle / sector) * sector - angle) * length(p) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
float sparkleStarMask(vec2 p, float points, float innerRatio) {
|
||||||
|
float angle = atan(p.y, p.x);
|
||||||
|
float radius = dot(p, p); // Optimized: Use squared distance to delay sqrt
|
||||||
|
float spikes = 0.5 + 0.5 * cos(angle * points);
|
||||||
|
float starRadius = mix(0.34 * innerRatio, 0.34, pow(spikes, 1.4));
|
||||||
|
return smoothstep(0.0009, -0.0009, radius - (starRadius * starRadius));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 rotatePoint(vec2 p, float rotation) {
|
||||||
|
float c = cos(rotation);
|
||||||
|
float s = sin(rotation);
|
||||||
|
return vec2(c * p.x - s * p.y, s * p.x + c * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sparkleRectangleMask(vec2 p, float halfWidth, float halfHeight, float rotation) {
|
||||||
|
vec2 q = rotatePoint(p, rotation);
|
||||||
|
float body = sdBox(q, vec2(halfWidth, halfHeight));
|
||||||
|
float bevel = sdBox(q, vec2(halfWidth * 0.78, halfHeight * 0.78));
|
||||||
|
float bodyMask = smoothstep(0.03, -0.03, body);
|
||||||
|
|
||||||
|
float centerLine = 1.0 - smoothstep(0.0, halfHeight * 1.25, abs(q.y));
|
||||||
|
float endFade = 1.0 - smoothstep(halfWidth * 0.55, halfWidth * 1.05, abs(q.x));
|
||||||
|
float streak = centerLine * endFade;
|
||||||
|
float bevelRing = smoothstep(0.025, -0.025, body) - smoothstep(0.025, -0.025, bevel);
|
||||||
|
return clamp(bodyMask * (0.58 + 0.42 * streak) + bevelRing * 0.25, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sparklePolygonMask(vec2 p, float sides, float aspectRatio, float rotation) {
|
||||||
|
vec2 q = rotatePoint(p, rotation);
|
||||||
|
q.x /= aspectRatio;
|
||||||
|
float poly = sdRegularPolygon(q, sides, 0.26);
|
||||||
|
return smoothstep(0.03, -0.03, poly);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sparkleShapeMask(vec2 p, vec2 id) {
|
||||||
|
float randomRotation = hash(id + 5.1) * TWO_PI;
|
||||||
|
if (uSparkleShapeKind < 0.5) {
|
||||||
|
return sparkleStarMask(p, uSparklePrimary, uSparkleSecondary);
|
||||||
|
}
|
||||||
|
if (uSparkleShapeKind < 1.5) {
|
||||||
|
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
|
||||||
|
}
|
||||||
|
if (uSparkleShapeKind < 2.5) {
|
||||||
|
return sparklePolygonMask(p, uSparklePrimary, uSparkleSecondary, uSparkleTertiary);
|
||||||
|
}
|
||||||
|
if (uSparkleShapeKind < 3.5) {
|
||||||
|
float sides = 5.0 + floor(hash(id + 9.4) * 4.0);
|
||||||
|
float aspect = 0.7 + hash(id + 13.1) * 0.7;
|
||||||
|
return sparklePolygonMask(p, sides, aspect, randomRotation);
|
||||||
|
}
|
||||||
|
return sparkleRectangleMask(p, uSparklePrimary, uSparkleSecondary, randomRotation * uSparkleTertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 styleHolographicSilver(vec2 uv, vec2 tilt, float time) {
|
||||||
|
vec2 p = uv * 2.0 - 1.0;
|
||||||
|
float flowA = noise(uv * 3.5 + vec2(time * 0.03, -time * 0.02));
|
||||||
|
float flowB = noise(uv * 7.0 + vec2(-time * 0.02, time * 0.03));
|
||||||
|
vec2 warp = p + 0.25 * vec2(flowA - 0.5, flowB - 0.5);
|
||||||
|
float ribbon = sin(dot(warp, vec2(6.5, 2.8)) + flowA * 5.0 + dot(tilt, vec2(2.2, -1.7)));
|
||||||
|
float plume = sin(length(warp + vec2(flowB - 0.5, flowA - 0.5)) * 9.0 - tilt.x * 3.0 + time * 0.2);
|
||||||
|
float phase = flowA * 1.8 + ribbon * 0.28 + plume * 0.22 + time * 0.03;
|
||||||
|
|
||||||
|
vec3 holo = rainbow(phase, 0.72, 1.0);
|
||||||
|
float blend = smoothstep(-0.65, 0.95, ribbon + plume * 0.55);
|
||||||
|
vec3 silver = vec3(0.62, 0.65, 0.70) * (0.85 + 0.15 * flowB);
|
||||||
|
|
||||||
|
return mix(silver, holo, 0.65 * blend);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 styleCrackedIce(vec2 uv, vec2 tilt, float time) {
|
||||||
|
float maxZ = -999.0;
|
||||||
|
vec2 bestSlope = vec2(0.0);
|
||||||
|
float bestHash = 0.0;
|
||||||
|
float bestEdge = 1.0;
|
||||||
|
|
||||||
|
vec2 warp = vec2(sin(uv.y * 10.0), cos(uv.x * 10.0)) * 0.05;
|
||||||
|
vec2 baseUV = uv + warp;
|
||||||
|
|
||||||
|
// --- LAYER 1 ---
|
||||||
|
vec2 p1 = baseUV * 12.0;
|
||||||
|
vec2 id1 = floor(p1);
|
||||||
|
vec2 f1 = fract(p1) - 0.5;
|
||||||
|
vec2 slope1 = vec2(hash(id1 + 1.2), hash(id1 + 1.3)) * 2.0 - 1.0;
|
||||||
|
float z1 = dot(f1, slope1) * 3.5;
|
||||||
|
if (z1 > maxZ) {
|
||||||
|
maxZ = z1; bestSlope = slope1; bestHash = hash(id1 + 1.1); bestEdge = min(0.5 - abs(f1.x), 0.5 - abs(f1.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LAYER 2 (Optimized Rotation) ---
|
||||||
|
vec2 p2 = (ROT_0_78 * baseUV) * 15.0;
|
||||||
|
vec2 id2 = floor(p2);
|
||||||
|
vec2 f2 = fract(p2) - 0.5;
|
||||||
|
vec2 slope2 = vec2(hash(id2 + 2.2), hash(id2 + 2.3)) * 2.0 - 1.0;
|
||||||
|
float z2 = 0.8 + dot(f2, slope2) * 3.5;
|
||||||
|
if (z2 > maxZ) {
|
||||||
|
maxZ = z2; bestSlope = slope2; bestHash = hash(id2 + 2.1); bestEdge = min(0.5 - abs(f2.x), 0.5 - abs(f2.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LAYER 3 (Optimized Rotation) ---
|
||||||
|
vec2 p3 = (ROT_M0_5 * baseUV) * 18.0;
|
||||||
|
vec2 id3 = floor(p3);
|
||||||
|
vec2 f3 = fract(p3) - 0.5;
|
||||||
|
vec2 slope3 = vec2(hash(id3 + 3.2), hash(id3 + 3.3)) * 2.0 - 1.0;
|
||||||
|
float z3 = 1.6 + dot(f3, slope3) * 3.5;
|
||||||
|
if (z3 > maxZ) {
|
||||||
|
maxZ = z3; bestSlope = slope3; bestHash = hash(id3 + 3.1); bestEdge = min(0.5 - abs(f3.x), 0.5 - abs(f3.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LAYER 4 (Optimized Rotation) ---
|
||||||
|
vec2 p4 = (ROT_1_2 * baseUV) * 21.0;
|
||||||
|
vec2 id4 = floor(p4);
|
||||||
|
vec2 f4 = fract(p4) - 0.5;
|
||||||
|
vec2 slope4 = vec2(hash(id4 + 4.2), hash(id4 + 4.3)) * 2.0 - 1.0;
|
||||||
|
float z4 = 2.4 + dot(f4, slope4) * 3.5;
|
||||||
|
if (z4 > maxZ) {
|
||||||
|
maxZ = z4; bestSlope = slope4; bestHash = hash(id4 + 4.1); bestEdge = min(0.5 - abs(f4.x), 0.5 - abs(f4.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- VIBRANT COLOR OPTICS ---
|
||||||
|
float alignment = dot(tilt, normalize(bestSlope + vec2(0.001)));
|
||||||
|
float phase = dot(uv, vec2(0.6, -0.4)) + time * 0.05 + bestHash * 0.6 + alignment * 0.5;
|
||||||
|
float saturation = 0.7 + 0.3 * hash(vec2(bestHash, 6.1));
|
||||||
|
vec3 baseColor = rainbow(phase, saturation, 1.0);
|
||||||
|
|
||||||
|
float lightCycle = sin(alignment * 4.0 + bestHash * TWO_PI + time * 0.15);
|
||||||
|
float ambient = 0.15 + 0.25 * max(0.0, lightCycle);
|
||||||
|
vec3 finalColor = baseColor * ambient;
|
||||||
|
float flash = pow(max(0.0, lightCycle), 5.0);
|
||||||
|
finalColor += mix(baseColor, vec3(1.0, 0.95, 0.85), 0.6) * flash * 0.55;
|
||||||
|
finalColor += baseColor * smoothstep(0.05, 0.0, bestEdge) * flash * 0.4;
|
||||||
|
|
||||||
|
return finalColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 styleSilverMosaic(vec2 uv, vec2 tilt, float time) {
|
||||||
|
// Setup grid - Scaled up 300% (18.0 -> 6.0)
|
||||||
|
vec2 g = uv * 6.0;
|
||||||
|
vec2 id = floor(g);
|
||||||
|
|
||||||
|
// Center coordinates around 0.0 for the radial math
|
||||||
|
vec2 f = fract(g) - 0.5;
|
||||||
|
|
||||||
|
// Real foil sheets often alternate the grain of the squares to catch light from all angles.
|
||||||
|
if (mod(id.x + id.y, 2.0) > 0.5) {
|
||||||
|
f = vec2(-f.y, f.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the radial vector (pointing outward from the center of the square)
|
||||||
|
vec2 radialDir = normalize(f + vec2(0.0001));
|
||||||
|
|
||||||
|
// Calculate the simulated light direction based on device tilt.
|
||||||
|
vec2 lightDir = normalize(tilt + vec2(0.001));
|
||||||
|
|
||||||
|
// --- THE OPTICS ---
|
||||||
|
// A radial highlight occurs where the radial vector aligns with the light vector.
|
||||||
|
float alignment = abs(dot(radialDir, lightDir));
|
||||||
|
|
||||||
|
// Raise to a high power to narrow the reflection into a sharp, focused beam
|
||||||
|
float highlight = pow(alignment, 12.0);
|
||||||
|
|
||||||
|
// --- THE DIFFRACTION (Rainbow) ---
|
||||||
|
float angleDiff = acos(alignment);
|
||||||
|
|
||||||
|
// 2D cross product to find which side of the highlight we are on
|
||||||
|
float side = sign(radialDir.x * lightDir.y - radialDir.y * lightDir.x);
|
||||||
|
|
||||||
|
// Phase shift based on tilt magnitude and time.
|
||||||
|
float basePhase = length(tilt) * 2.5 + time * 0.1;
|
||||||
|
|
||||||
|
// Calculate the final color phase.
|
||||||
|
float phase = basePhase + side * angleDiff * 1.8;
|
||||||
|
vec3 holoColor = rainbow(phase, 0.85, 1.0);
|
||||||
|
|
||||||
|
// Add a pure white "specular core" where alignment is absolutely perfect
|
||||||
|
vec3 core = vec3(1.0) * smoothstep(0.98, 1.0, alignment);
|
||||||
|
|
||||||
|
// Base foil material
|
||||||
|
vec3 foilBase = vec3(0.25, 0.27, 0.30);
|
||||||
|
|
||||||
|
// Combine the light components
|
||||||
|
vec3 finalColor = foilBase + (holoColor * highlight * 1.5) + (core * 0.8);
|
||||||
|
|
||||||
|
// --- BORDERS AND DEPTH ---
|
||||||
|
// Crisp borders between the squares
|
||||||
|
float edgeX = 0.5 - abs(f.x);
|
||||||
|
float edgeY = 0.5 - abs(f.y);
|
||||||
|
|
||||||
|
// Tightened the border width to compensate for the larger tile size
|
||||||
|
float border = smoothstep(0.0, 0.015, min(edgeX, edgeY));
|
||||||
|
|
||||||
|
// Add a subtle darkening toward the edges of the tiles
|
||||||
|
float dist = length(f);
|
||||||
|
finalColor *= mix(0.7, 1.0, 1.0 - smoothstep(0.0, 0.5, dist) * 0.3);
|
||||||
|
|
||||||
|
return clamp(finalColor * border, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 styleSuperGoldVinyl(vec2 uv, vec2 tilt, float time) {
|
||||||
|
vec2 g = uv * 60.0;
|
||||||
|
vec2 staggered = vec2(g.x + 0.5 * mod(floor(g.y), 2.0), g.y);
|
||||||
|
vec2 f = fract(staggered) - 0.5;
|
||||||
|
|
||||||
|
// Optimized: Use dot product for squared distance to avoid expensive sqrt/length.
|
||||||
|
// Original smoothstep values (0.42, 0.10) squared become (0.1764, 0.0100)
|
||||||
|
// Original smoothstep values (0.45, 0.18) squared become (0.2025, 0.0324)
|
||||||
|
float d2 = dot(f, f);
|
||||||
|
float dotMask = smoothstep(0.1764, 0.01, d2);
|
||||||
|
float emboss = smoothstep(0.2025, 0.0324, d2);
|
||||||
|
|
||||||
|
float sheen = 0.5 + 0.5 * sin(dot(uv, vec2(18.0, -6.0)) + noise(uv * 6.0) * 2.5 + dot(tilt, vec2(2.4, 1.8)) * 2.0 + time * 0.2);
|
||||||
|
vec3 gold = vec3(0.84, 0.70, 0.24);
|
||||||
|
vec3 warm = vec3(1.0, 0.88, 0.42);
|
||||||
|
vec3 shadow = vec3(0.18, 0.14, 0.05);
|
||||||
|
|
||||||
|
return gold * (0.75 + 0.25 * sheen) + warm * dotMask * 0.28 + shadow * emboss * 0.08;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Standard normalized UVs [0.0 to 1.0] for macro lighting, tilt, and glare
|
||||||
|
vec2 uv = FlutterFragCoord().xy / uSize;
|
||||||
|
vec2 center = vec2(0.5, 0.5);
|
||||||
|
vec2 fromCenter = uv - center;
|
||||||
|
|
||||||
|
// Aspect-corrected UVs for physical patterns (grid, sparkles, mosaics)
|
||||||
|
// Dividing by the maximum dimension ensures squares remain squares.
|
||||||
|
float maxDim = max(uSize.x, uSize.y);
|
||||||
|
vec2 aspectUV = FlutterFragCoord().xy / maxDim;
|
||||||
|
|
||||||
|
float tiltMag = length(uTilt);
|
||||||
|
float safeTiltMag = max(tiltMag, 0.001);
|
||||||
|
vec2 tiltDir = uTilt / safeTiltMag;
|
||||||
|
|
||||||
|
// The highlight uses the stretched UV so the glare covers the whole widget nicely
|
||||||
|
float highlightDistance = length(uv - (center + (uTilt * 0.35)));
|
||||||
|
float specularMask = pow(max(0.0, 1.0 - highlightDistance * 2.6), 3.2);
|
||||||
|
vec3 specular = vec3(specularMask) * uSpecular * (0.4 + 0.6 * tiltMag);
|
||||||
|
|
||||||
|
bool isCrackedIce = uStyle >= 0.5 && uStyle < 1.5;
|
||||||
|
float sparkleGridScale = isCrackedIce ? 7.0 : 18.0;
|
||||||
|
|
||||||
|
// ---> USE aspectUV HERE so sparkles are drawn perfectly proportionally
|
||||||
|
vec2 sparkleGrid = aspectUV * sparkleGridScale;
|
||||||
|
vec2 grid = floor(sparkleGrid);
|
||||||
|
|
||||||
|
float rarityThreshold = isCrackedIce ? 0.965 : 0.83;
|
||||||
|
float rarity = step(rarityThreshold, hash(grid + 0.31));
|
||||||
|
|
||||||
|
vec3 sparkle = vec3(0.0);
|
||||||
|
if (rarity > 0.0) {
|
||||||
|
vec2 cell = fract(sparkleGrid) - 0.5;
|
||||||
|
float sparkleHash = hash(grid);
|
||||||
|
vec2 sparkleNormal = normalize(vec2(sparkleHash * 2.0 - 1.0, hash(grid + 19.7) * 2.0 - 1.0));
|
||||||
|
float alignment = max(0.0, dot(tiltDir, sparkleNormal));
|
||||||
|
|
||||||
|
if (alignment > 0.0) {
|
||||||
|
float twinkle = 0.5 + 0.5 * sin(uTime * 5.5 + sparkleHash * TWO_PI);
|
||||||
|
float shapeMask = 0.0;
|
||||||
|
|
||||||
|
if (isCrackedIce) {
|
||||||
|
float sides = 5.0 + floor(hash(grid + 9.4) * 4.0);
|
||||||
|
float aspect = 0.75 + hash(grid + 13.1) * 0.55;
|
||||||
|
shapeMask = sparklePolygonMask(cell, sides, aspect, hash(grid + 5.1) * TWO_PI);
|
||||||
|
} else {
|
||||||
|
shapeMask = sparkleShapeMask(cell, grid);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sparklePower = isCrackedIce ? 4.8 : 3.2;
|
||||||
|
float sparkleIntensity = isCrackedIce ? 0.25 : 1.0;
|
||||||
|
float sparkleChroma = isCrackedIce ? 0.20 : 0.35;
|
||||||
|
float sparkleMask = pow(alignment, sparklePower) * twinkle * rarity * shapeMask;
|
||||||
|
vec3 sparkleColor = mix(vec3(1.0), rainbow(sparkleHash + dot(uTilt, vec2(0.2, -0.15)), 0.55, 1.0), sparkleChroma);
|
||||||
|
sparkle = sparkleColor * sparkleMask * uSparkle * sparkleIntensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 styleBase;
|
||||||
|
// ---> USE aspectUV HERE so the foil patterns tile properly
|
||||||
|
if (uStyle < 0.5) {
|
||||||
|
styleBase = styleHolographicSilver(aspectUV, uTilt, uTime);
|
||||||
|
} else if (uStyle < 1.5) {
|
||||||
|
styleBase = styleCrackedIce(aspectUV, uTilt, uTime);
|
||||||
|
} else if (uStyle < 2.5) {
|
||||||
|
styleBase = styleSilverMosaic(aspectUV, uTilt, uTime);
|
||||||
|
} else {
|
||||||
|
styleBase = styleSuperGoldVinyl(aspectUV, uTilt, uTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
float styleLuma = dot(styleBase, vec3(0.299, 0.587, 0.114));
|
||||||
|
vec3 chromaAdjusted = mix(vec3(styleLuma), styleBase, 0.2 + 0.8 * uPrismatic);
|
||||||
|
|
||||||
|
// General 3D lighting normal uses the stretched UV
|
||||||
|
vec2 normal2d = normalize(fromCenter + vec2(0.001));
|
||||||
|
float directional = 0.5 + 0.5 * dot(normal2d, tiltDir);
|
||||||
|
float tiltLighting = mix(0.86, 1.20, pow(directional, 1.2));
|
||||||
|
chromaAdjusted *= tiltLighting;
|
||||||
|
|
||||||
|
float microStrength = isCrackedIce ? 0.08 : 0.18;
|
||||||
|
|
||||||
|
// ---> USE aspectUV HERE so the microscopic diffraction doesn't stretch
|
||||||
|
float microMask = 0.5 + 0.5 * sin(dot(aspectUV, vec2(137.0, 57.0)) + noise(aspectUV * 14.0) * 4.0 + dot(uTilt, vec2(9.0, 4.0)) * 2.0);
|
||||||
|
vec3 microShimmer = rainbow(noise(aspectUV * 10.0) + dot(uTilt, vec2(0.4, -0.3)) * 0.3, 0.65, 1.0) * microMask * uDiffraction * microStrength;
|
||||||
|
|
||||||
|
vec3 styleColor = chromaAdjusted + microShimmer + (specular * 0.15) + sparkle;
|
||||||
|
vec3 mappedColor = styleColor / (1.0 + styleColor * 0.3);
|
||||||
|
float luma = dot(mappedColor, vec3(0.299, 0.587, 0.114));
|
||||||
|
vec3 vibrantColor = mix(vec3(luma), mappedColor, 1.4);
|
||||||
|
|
||||||
|
float foilBrightness = dot(vibrantColor, vec3(0.333));
|
||||||
|
float alpha = clamp(foilBrightness * 0.5, 0.0, 0.35);
|
||||||
|
vec3 finalColor = clamp(vibrantColor, 0.0, 1.0);
|
||||||
|
|
||||||
|
fragColor = vec4(finalColor * alpha, alpha);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:holo_shiny/holo_shiny.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('ShinyController uses external stream when provided', () async {
|
||||||
|
final StreamController<Offset> source =
|
||||||
|
StreamController<Offset>.broadcast();
|
||||||
|
final ShinyController controller =
|
||||||
|
ShinyController(useSensor: true, tiltStream: source.stream);
|
||||||
|
|
||||||
|
final Future<Offset> next = controller.stream.first;
|
||||||
|
source.add(const Offset(0.5, -0.25));
|
||||||
|
|
||||||
|
expect(await next, const Offset(0.5, -0.25));
|
||||||
|
|
||||||
|
await controller.dispose();
|
||||||
|
await source.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ShinyController emits nothing by default', () async {
|
||||||
|
final ShinyController controller = ShinyController();
|
||||||
|
final bool hasEvent = await controller.stream.isEmpty;
|
||||||
|
expect(hasEvent, isTrue);
|
||||||
|
await controller.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:holo_shiny/holo_shiny.dart';
|
||||||
|
|
||||||
|
const ValueKey<String> _cardTransformKey =
|
||||||
|
ValueKey<String>('holo_shiny.card.transform');
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('default holograph style is crackedIce', () {
|
||||||
|
const Shiny shiny = Shiny(child: SizedBox.shrink());
|
||||||
|
const ShinyCard shinyCard = ShinyCard();
|
||||||
|
|
||||||
|
expect(shiny.style, HolographStyle.crackedIce);
|
||||||
|
expect(shinyCard.style, HolographStyle.crackedIce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('default sparkle shape is star', () {
|
||||||
|
const Shiny shiny = Shiny(child: SizedBox.shrink());
|
||||||
|
const ShinyCard shinyCard = ShinyCard();
|
||||||
|
|
||||||
|
expect(shiny.sparkleShape.kind, SparkleShapeKind.star);
|
||||||
|
expect(shiny.sparkleShape.primary, 8.0);
|
||||||
|
expect(shinyCard.sparkleShape.kind, SparkleShapeKind.star);
|
||||||
|
expect(shinyCard.sparkleShape.primary, 8.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('custom sparkle factories produce expected specs', () {
|
||||||
|
final SparkleShapeSpec star =
|
||||||
|
SparkleShapeSpec.customStar(points: 7, innerRatio: 0.33);
|
||||||
|
final SparkleShapeSpec polygon = SparkleShapeSpec.customPolygon(
|
||||||
|
sides: 8, aspectRatio: 1.2, rotation: 0.4);
|
||||||
|
final SparkleShapeSpec rectangle = SparkleShapeSpec.customRectangle(
|
||||||
|
halfWidth: 0.20, halfHeight: 0.05, rotationJitter: 0.7);
|
||||||
|
|
||||||
|
expect(star.kind, SparkleShapeKind.star);
|
||||||
|
expect(star.primary, 7.0);
|
||||||
|
expect(star.secondary, 0.33);
|
||||||
|
|
||||||
|
expect(polygon.kind, SparkleShapeKind.polygon);
|
||||||
|
expect(polygon.primary, 8.0);
|
||||||
|
expect(polygon.secondary, 1.2);
|
||||||
|
expect(polygon.tertiary, 0.4);
|
||||||
|
|
||||||
|
expect(rectangle.kind, SparkleShapeKind.rectangle);
|
||||||
|
expect(rectangle.primary, 0.20);
|
||||||
|
expect(rectangle.secondary, 0.05);
|
||||||
|
expect(rectangle.tertiary, 0.7);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Shiny wraps any child without card transform',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Shiny(
|
||||||
|
enableShader: false,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Colors.blue,
|
||||||
|
child: SizedBox(width: 80, height: 40),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(ColoredBox), findsWidgets);
|
||||||
|
expect(find.byKey(_cardTransformKey), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ShinyCard renders clipping and foreground',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: ShinyCard(
|
||||||
|
enableShader: false,
|
||||||
|
background: ColoredBox(color: Colors.black),
|
||||||
|
foreground: Text('FOREGROUND'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('FOREGROUND'), findsOneWidget);
|
||||||
|
expect(find.byType(ClipPath), findsOneWidget);
|
||||||
|
expect(find.byKey(_cardTransformKey), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('ShinyCard responds to external tilt stream',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
final StreamController<Offset> stream =
|
||||||
|
StreamController<Offset>.broadcast();
|
||||||
|
final ShinyController controller =
|
||||||
|
ShinyController(tiltStream: stream.stream);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: ShinyCard(
|
||||||
|
enableShader: false,
|
||||||
|
controller: controller,
|
||||||
|
background: const ColoredBox(color: Colors.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Matrix4 before =
|
||||||
|
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||||
|
|
||||||
|
stream.add(const Offset(0.7, 0.2));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final Matrix4 after =
|
||||||
|
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||||
|
|
||||||
|
expect(after.storage[8], isNot(before.storage[8]));
|
||||||
|
|
||||||
|
await controller.dispose();
|
||||||
|
await stream.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Dragging ShinyCard updates transform',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: ShinyCard(
|
||||||
|
enableShader: false,
|
||||||
|
background: ColoredBox(color: Colors.black),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Finder shinyCard = find.byType(ShinyCard);
|
||||||
|
final Matrix4 before =
|
||||||
|
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||||
|
|
||||||
|
await tester.drag(shinyCard, const Offset(50, 20));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final Matrix4 after =
|
||||||
|
tester.widget<Transform>(find.byKey(_cardTransformKey)).transform;
|
||||||
|
expect(after.storage[8], isNot(before.storage[8]));
|
||||||
|
});
|
||||||
|
}
|
||||||